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
13 var ajaxCategories = function ( options
) {
14 // TODO grab these out of option object.
16 var catLinkWrapper
= '<li/>';
17 var $container
= $( '.catlinks' );
18 var $containerNormal
= $( '#mw-normal-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 ( wgUserGroups
.indexOf("user") != -1 && !all
) {
174 _stash
.summaries
.push( actionSummary
);
175 _stash
.fns
.push( fn
);
177 _saveAllButton
.show();
179 // This only does visual changes
183 // Produce a confirmation dialog
184 var dialog
= $( '<div/>' );
186 dialog
.addClass( 'mw-ajax-confirm-dialog' );
187 dialog
.attr( 'title', mw
.msg( 'ajax-confirm-title' ) );
190 var confirmIntro
= $( '<p/>' );
191 confirmIntro
.text( mw
.msg( 'ajax-confirm-prompt' ) );
192 dialog
.append( confirmIntro
);
194 // Summary of the action to be taken
195 var summaryHolder
= $( '<p/>' );
196 var summaryLabel
= $( '<strong/>' );
197 summaryLabel
.text( mw
.msg( 'ajax-confirm-actionsummary' ) + " " );
198 summaryHolder
.text( actionSummary
);
199 summaryHolder
.prepend( summaryLabel
);
200 dialog
.append( summaryHolder
);
203 var reasonBox
= $( '<input type="text" size="45" />' );
204 reasonBox
.addClass( 'mw-ajax-confirm-reason' );
205 dialog
.append( reasonBox
);
208 var submitButton
= $( '<input type="button"/>' );
209 submitButton
.val( mw
.msg( 'ajax-confirm-save' ) );
211 var submitFunction = function() {
212 _addProgressIndicator( dialog
);
214 mw
.config
.get( 'wgPageName' ),
219 dialog
.dialog( 'close' );
220 _removeProgressIndicator( dialog
);
226 buttons
[mw
.msg( 'ajax-confirm-save' )] = submitFunction
;
227 var dialogOptions
= {
233 $( '#catlinks' ).prepend( dialog
);
234 dialog
.dialog( dialogOptions
);
238 * When multiEdit mode is enabled,
239 * this is called when the user clicks "save all"
240 * Combines the summaries and edit functions
242 _handleStashedCategories = function() {
247 var summary
= _stash
.summaries
.join('. ');
248 var combinedFn = function( oldtext
) {
249 // Run the text through all action functions
251 for ( var i
= 0; i
< fns
.length
; i
++ ) {
252 newtext
= fns
[i
]( newtext
);
256 var doneFn = function() {
257 //Remove saveAllButton
258 _saveAllButton
.hide();
262 _stash
.summaries
= [];
265 // Any link with $link.css('text-decoration', 'line-through');
266 // needs to be removed
268 _confirmEdit( combinedFn
, summary
, doneFn
, true );
271 _doEdit = function ( page
, fn
, summary
, doneFn
) {
272 // Get an edit token for the page.
275 'prop':'info|revisions',
278 'rvprop':'content|timestamp',
282 $.get( mw
.util
.wikiScript( 'api' ), getTokenVars
,
284 var infos
= reply
.query
.pages
;
287 function( pageid
, data
) {
288 var token
= data
.edittoken
;
289 var timestamp
= data
.revisions
[0].timestamp
;
290 var oldText
= data
.revisions
[0]['*'];
292 var newText
= fn( oldText
);
294 if ( newText
=== false ) return;
302 'basetimestamp':timestamp
,
306 $.post( mw
.util
.wikiScript( 'api' ), postEditVars
, doneFn
, 'json' );
314 * Append spinner wheel to element
315 * @param DOMObject element.
317 _addProgressIndicator = function ( elem
) {
318 var indicator
= $( '<div/>' );
320 indicator
.addClass( 'mw-ajax-loader' );
322 elem
.append( indicator
);
326 * Find and remove spinner wheel from inside element
327 * @param DOMObject parent element.
329 _removeProgressIndicator = function ( elem
) {
330 elem
.find( '.mw-ajax-loader' ).remove();
334 * Makes regex string caseinsensitive.
335 * Useful when 'i' flag can't be used.
336 * Return stuff like [Ff][Oo][Oo]
337 * @param string Regex string.
338 * @return string Processed regex string
340 _makeCaseInsensitive = function ( string
) {
342 for (var i
=0; i
< string
.length
; i
++) {
343 newString
+= '[' + string
[i
].toUpperCase() + string
[i
].toLowerCase() + ']';
347 _buildRegex = function ( category
) {
348 // Build a regex that matches legal invocations of that category.
349 var categoryNSFragment
= '';
350 $.each( namespaceIds
, function( name
, id
) {
352 // The parser accepts stuff like cATegORy,
353 // we need to do the same
354 categoryNSFragment
+= '|' + _makeCaseInsensitive ( $.escapeRE(name
) );
357 categoryNSFragment
= categoryNSFragment
.substr( 1 ); // Remove leading |
360 var titleFragment
= $.escapeRE(category
);
362 firstChar
= category
.charAt( 0 );
363 firstChar
= '[' + firstChar
.toUpperCase() + firstChar
.toLowerCase() + ']';
364 titleFragment
= firstChar
+ category
.substr( 1 );
365 var categoryRegex
= '\\[\\[(' + categoryNSFragment
+ '):' + titleFragment
+ '(\\|[^\\]]*)?\\]\\]';
367 return new RegExp( categoryRegex
, 'g' );
370 _handleEditLink = function ( e
) {
372 var $this = $( this );
373 var $link
= $this.parent().find( 'a:not(.icon)' );
374 var category
= $link
.text();
376 var $input
= _makeSuggestionBox( category
, _handleCategoryEdit
, mw
.msg( 'ajax-confirm-save' ) );
377 $link
.after( $input
).hide();
378 _catElements
[category
].editButton
.hide();
379 _catElements
[category
].deleteButton
.unbind('click').click( function() {
382 _catElements
[category
].editButton
.show();
383 $( this ).unbind('click').click( _handleDeleteLink
);
387 _handleAddLink = function ( e
) {
390 $container
.find( '#mw-normal-catlinks>.mw-addcategory-prompt' ).toggle();
393 _handleDeleteLink = function ( e
) {
396 var $this = $( this );
397 var $link
= $this.parent().find( 'a:not(.icon)' );
398 var category
= $link
.text();
400 var categoryRegex
= _buildRegex( category
);
402 var summary
= mw
.msg( 'ajax-remove-category-summary', category
);
405 function( oldText
) {
406 newText
= _runHooks ( oldText
, 'beforeDelete' );
407 //TODO Cleanup whitespace safely?
408 var newText
= newText
.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' );
421 function( unsaved
) {
423 //TODO flesh out: Make it a class, make revertable
424 $link
.css('text-decoration', 'line-through');
426 $this.parent().remove();
432 _handleCategoryAdd = function ( e
) {
433 // Grab category text
434 var category
= $( this ).parent().find( '.mw-addcategory-input' ).val();
435 category
= $.ucFirst( category
);
437 if ( _containsCat(category
) ) {
438 _showError( mw
.msg( 'ajax-category-already-present' ) );
441 var appendText
= "\n[[" + categoryNamespace
+ ":" + category
+ "]]\n";
442 var summary
= mw
.msg( 'ajax-add-category-summary', category
);
445 function( oldText
) {
446 newText
= _runHooks ( oldText
, 'beforeAdd' );
447 return newText
+ appendText
;
451 _insertCatDOM( category
, false );
456 _handleCategoryEdit = function ( e
) {
459 // Grab category text
460 var categoryNew
= $( this ).parent().find( '.mw-addcategory-input' ).val();
461 categoryNew
= $.ucFirst( categoryNew
);
463 var $this = $( this );
464 var $link
= $this.parent().parent().find( 'a:not(.icon)' );
465 var category
= $link
.text();
467 // User didn't change anything. Just close the box
468 if ( category
== categoryNew
) {
469 $this.parent().remove();
473 categoryRegex
= _buildRegex( category
);
475 var summary
= mw
.msg( 'ajax-edit-category-summary', category
, categoryNew
);
478 function( oldText
) {
479 newText
= _runHooks ( oldText
, 'beforeChange' );
481 var matches
= newText
.match( categoryRegex
);
483 //Old cat wasn't found, likely to be transcluded
484 if ( !$.isArray( matches
) ) {
485 var error
= mw
.msg( 'ajax-edit-category-error' );
487 _removeProgressIndicator( $( '.mw-ajax-confirm-dialog' ) );
488 $( '.mw-ajax-confirm-dialog' ).dialog( 'close' );
491 var sortkey
= matches
[0].replace( categoryRegex
, '$2' );
492 var newCategoryString
= "[[" + categoryNamespace
+ ":" + categoryNew
+ sortkey
+ ']]';
494 if (matches
.length
> 1) {
495 // The category is duplicated.
496 // Remove all but one match
497 for (var i
= 1; i
< matches
.length
; i
++) {
498 oldText
= oldText
.replace( matches
[i
], '');
501 var newText
= oldText
.replace( categoryRegex
, newCategoryString
);
507 // Remove input box & button
508 $this.parent().remove();
510 // Update link text and href
511 $link
.show().text( categoryNew
).attr( 'href', _catLink( categoryNew
) );
517 * Open a dismissable error dialog
519 * @param string str The error description
521 _showError = function ( str
) {
522 var dialog
= $( '<div/>' );
525 $( '#bodyContent' ).append( dialog
);
528 buttons
[mw
.msg( 'ajax-error-dismiss' )] = function( e
) {
529 dialog
.dialog( 'close' );
531 var dialogOptions
= {
534 'title' : mw
.msg( 'ajax-error-title' )
537 dialog
.dialog( dialogOptions
);
541 * Manufacture iconed button, with or without text
543 * @param string icon The icon class.
544 * @param string title Title attribute.
545 * @param string className (optional) Additional classes to be added to the button.
546 * @param string text (optional) Text of button.
548 * @return jQueryObject The button
550 _createButton = function ( icon
, title
, className
, text
){
551 var $button
= $( '<a>' ).addClass( className
|| '' )
552 .attr('title', title
);
555 var $icon
= $( '<a>' ).addClass( 'icon ' + icon
);
556 $button
.addClass( 'icon-parent' ).append( $icon
).append( text
);
558 $button
.addClass( 'icon ' + icon
);
564 * Append edit and remove buttons to a given category link
566 * @param DOMElement element Anchor element, to which the buttons should be appended.
568 _createCatButtons = function( element
) {
569 // Create remove & edit buttons
570 var deleteButton
= _createButton('icon-close', mw
.msg( 'ajax-remove-category' ) );
571 var editButton
= _createButton('icon-edit', mw
.msg( 'ajax-edit-category' ) );
574 var saveButton
= _createButton('icon-tick', mw
.msg( 'ajax-confirm-save' ) ).hide();
576 deleteButton
.click( _handleDeleteLink
);
577 editButton
.click( _handleEditLink
);
579 $( element
).after( deleteButton
).after( editButton
);
581 //Save references to all links and buttons
582 _catElements
[$( element
).text()] = {
584 parent
: $( element
).parent(),
585 saveButton
: saveButton
,
586 deleteButton
: deleteButton
,
587 editButton
: editButton
590 this.setup = function () {
591 // Could be set by gadgets like HotCat etc.
592 if ( mw
.config
.get('disableAJAXCategories') ) {
595 // Only do it for articles.
596 if ( !mw
.config
.get( 'wgIsArticle' ) ) return;
598 // Unhide hidden category holders.
599 $('#mw-hidden-catlinks').show();
601 // Create [Add Category] link
602 var addLink
= _createButton('icon-add',
603 mw
.msg( 'ajax-add-category' ),
604 'mw-ajax-addcategory',
605 mw
.msg( 'ajax-add-category' )
607 addLink
.click( _handleAddLink
);
608 $containerNormal
.append( addLink
);
610 // Create add category prompt
611 var promptContainer
= _makeSuggestionBox( '', _handleCategoryAdd
, mw
.msg( 'ajax-add-category-submit' ) );
612 promptContainer
.hide();
614 // Create edit & delete link for each category.
615 $( '#catlinks li a' ).each( function( e
) {
616 _createCatButtons( this );
619 $containerNormal
.append( promptContainer
);
621 //TODO Make more clickable
622 _saveAllButton
= _createButton( 'icon-tick',
623 mw
.msg( 'ajax-confirm-save-all' ),
625 mw
.msg( 'ajax-confirm-save-all' )
627 _saveAllButton
.click( _handleStashedCategories
).hide();
628 $containerNormal
.append( _saveAllButton
);
640 _runHooks = function( oldtext
, type
) {
641 // No hooks registered
642 if ( !_hooks
[type
] ) {
645 for (var i
= 0; i
< _hooks
[type
].length
; i
++) {
646 oldtext
= _hooks
[type
][i
]( oldtext
);
653 * Currently available: beforeAdd, beforeChange, beforeDelete
655 * @param string type Type of hook to add
656 * @param function fn Hook function. This function is the old text passed
657 * and it needs to return the modified text
659 this.addHook = function( type
, fn
) {
660 if ( !_hooks
[type
] ) return;
661 else hooks
[type
].push( fn
);
664 // Now make a new version
665 mw
.ajaxCategories
= new ajaxCategories();
667 // Executing only on doc.ready, so that everyone
668 // gets a chance to set mw.config.set('disableAJAXCategories')
669 $( document
).ready( mw
.ajaxCategories
.setup() );
671 } )( jQuery
, mediaWiki
);