Followup r93343
[lhc/web/wiklou.git] / resources / mediawiki.page / mediawiki.page.ajaxCategories.js
1 /**
2 * mediaWiki.page.ajaxCategories
3 *
4 * @author Michael Dale, 2009
5 * @author Leo Koppelkamm, 2011
6 * @since 1.18
7 *
8 * Relies on: mw.config (wgFormattedNamespaces, wgNamespaceIds, wgCaseSensitiveNamespaces, wgUserGroups),
9 * mw.util.wikiGetlink, mw.user.getId
10 */
11 ( function( $ ) {
12
13 /* Local scope */
14
15 var catNsId = mw.config.get( 'wgNamespaceIds' ).category,
16
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, '' );
20 }
21 },
22
23 /**
24 * Build URL for passed Category
25 *
26 * @param string category name.
27 * @return string Valid URL
28 */
29 catUrl = function( cat ) {
30 return mw.util.wikiGetlink( new mw.Title( cat, catNsId ) );
31 };
32
33 /**
34 * Helper function for $.fn.suggestion
35 *
36 * @param string Query string.
37 */
38 fetchSuggestions = function( query ) {
39 var _this = this;
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' ),
44 data: {
45 'action': 'query',
46 'list': 'allpages',
47 'apnamespace': catNsId,
48 'apprefix': catName,
49 'format': 'json'
50 },
51 dataType: 'json',
52 success: function( data ) {
53 // Process data.query.allpages into an array of titles
54 var pages = data.query.allpages;
55 var titleArr = [];
56
57 $.each( pages, function( i, page ) {
58 var title = page.title.split( ':', 2 )[1];
59 titleArr.push( title );
60 } );
61
62 $( _this ).suggestions( 'suggestions', titleArr );
63 }
64 } );
65 _request = request;
66 };
67
68 /**
69 * Replace <nowiki> and comments with unique keys
70 */
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 + '-' );
76 }
77 return text;
78 };
79
80 /**
81 * Restore <nowiki> and comments from unique keys
82 */
83 restoreNowikis = function( text, id, array ) {
84 for ( var i = 0; i < array.length; i++ ) {
85 text = text.replace( id + i + '-', array[i] );
86 }
87 return text;
88 };
89
90 /**
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
96 */
97 makeCaseInsensitive = function( string ) {
98 if ( $.inArray( 14, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) + 1 ) {
99 return string;
100 }
101 var newString = '';
102 for ( var i=0; i < string.length; i++ ) {
103 newString += '[' + string.charAt( i ).toUpperCase() + string.charAt( i ).toLowerCase() + ']';
104 }
105 return newString;
106 };
107
108 /**
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?
113 * @return Regex
114 */
115 buildRegex = function( category, matchLineBreak ) {
116 var categoryNSFragment = '';
117 $.each( mw.config.get( 'wgNamespaceIds' ), function( name, id ) {
118 if ( id == 14 ) {
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 ) );
123 }
124 } );
125 categoryNSFragment = categoryNSFragment.substr( 1 ); // Remove leading pipe
126
127 // Build the regex
128 var titleFragment = $.escapeRE( category ).replace( /( |_)/g, '[ _]' );
129
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?';
136 }
137 return new RegExp( categoryRegex, 'g' );
138 };
139
140
141 mw.ajaxCategories = function( options ) {
142 //Save scope in shortcut
143 var that = this, _request, _saveAllButton, _cancelAllButton, _addContainer, defaults;
144
145 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
152 };
153 // merge defaults and options, without modifying defaults */
154 options = $.extend( {}, defaults, options );
155
156 /**
157 * Insert a newly added category into the DOM
158 *
159 * @param string category name.
160 * @return jQuery object
161 */
162 this.createCatLink = function( cat ) {
163 // User can implicitely state a sort key.
164 // Remove before display
165 cat = cat.replace(/\|.*/, '' );
166
167 // strip out bad characters
168 cat = clean ( cat );
169
170 if ( $.isEmpty( cat ) || that.containsCat( cat ) ) {
171 return;
172 }
173
174 var $catLinkWrapper = $( options.catLinkWrapper );
175 var $anchor = $( '<a/>' ).append( cat );
176 $catLinkWrapper.append( $anchor );
177 $anchor.attr( { target: "_blank", href: catUrl( cat ) } );
178
179 _createCatButtons( $anchor );
180
181 return $anchor;
182 };
183
184 /**
185 * Takes a category link element
186 * and strips all data from it.
187 *
188 * @param jQuery object
189 */
190 this.resetCatLink = function( $link, del, dontRestoreText ) {
191 $link.removeClass( 'mw-removed-category mw-added-category mw-changed-category' );
192 var data = $link.data();
193
194 if ( typeof data.stashIndex == "number" ) {
195 _removeStashItem( data.stashIndex );
196 }
197 if ( del ) {
198 $link.parent.remove();
199 return;
200 }
201 if ( data.origCat && !dontRestoreText ) {
202 $link.text( data.origCat );
203 $link.attr( 'href', catUrl( data.origCat ) );
204 }
205
206 $link.removeData();
207
208 //Readd static.
209 $link.data({
210 saveButton : data.saveButton,
211 deleteButton: data.deleteButton,
212 editButton : data.editButton
213 });
214 };
215
216 /**
217 * Reset all data from the category links and the stash.
218 * @param Boolean del Delete any category links with .mw-removed-category
219 */
220 this.resetAll = function( del ) {
221 var $links = options.$container.find( options.categoryLinkSelector ), $del = $();
222 if ( del ) {
223 $del = $links.filter( '.mw-removed-category' ).parent();
224 }
225
226 $links.each( function() {
227 that.resetCatLink( $( this ), false, del );
228 });
229
230 $del.remove();
231
232 if ( !options.$container.find( '#mw-hidden-catlinks li' ).length ) {
233 options.$container.find( '#mw-hidden-catlinks' ).remove();
234 }
235 };
236
237 /**
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
242 */
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 );
249 }
250 var addButton = $( '<input type="button" class="mw-addcategory-button"/>' );
251 addButton.val( buttonVal );
252
253 addButton.click( callback );
254 promptTextbox.keyup( function( e ) {
255 if ( e.keyCode == 13 ) addButton.click();
256 });
257 promptTextbox.suggestions( {
258 'fetch': fetchSuggestions,
259 'cancel': function() {
260 var req = _request;
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 ) {
263 req.abort();
264 }
265 }
266 } );
267
268 promptTextbox.suggestions();
269
270 promptContainer.append( promptTextbox );
271 promptContainer.append( addButton );
272
273 return promptContainer;
274 };
275
276 /**
277 * Parse the DOM $container and build a list of
278 * present categories
279 *
280 * @return array Array of all categories
281 */
282 this.getCats = function() {
283 return options.$container.find( options.categoryLinkSelector ).map( function() { return $.trim( $( this ).text() ); } );
284 };
285
286 /**
287 * Check whether a passed category is present in the DOM
288 *
289 * @return boolean True for exists
290 */
291 this.containsCat = function( cat ) {
292 return that.getCats().filter( function() { return $.ucFirst( this ) == $.ucFirst( cat ); } ).length !== 0;
293 };
294
295 /**
296 * This gets called by all action buttons
297 * Displays a dialog to confirm the action
298 * Afterwards do the actual edit
299 *
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
305 */
306 this._confirmEdit = function( fn, actionSummary, shortSummary, doneFn, $link, action ) {
307 // Check whether to use multiEdit mode
308 if ( options.multiEdit && action != 'all' ) {
309 // Stash away
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 );
315
316 _saveAllButton.show();
317 _cancelAllButton.show();
318
319 // This only does visual changes
320 doneFn( true );
321 return;
322 }
323 // Produce a confirmation dialog
324 var dialog = $( '<div/>' );
325
326 dialog.addClass( 'mw-ajax-confirm-dialog' );
327 dialog.attr( 'title', mw.msg( 'ajax-confirm-title' ) );
328
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 );
333
334 // Reason textbox.
335 var reasonBox = $( '<input type="text" size="45" />' );
336 reasonBox.addClass( 'mw-ajax-confirm-reason' );
337 dialog.append( reasonBox );
338
339 // Submit button
340 var submitButton = $( '<input type="button"/>' );
341 submitButton.val( mw.msg( 'ajax-confirm-save' ) );
342
343 var submitFunction = function() {
344 that._addProgressIndicator( dialog );
345 that._doEdit(
346 mw.config.get( 'wgPageName' ),
347 fn,
348 shortSummary + ': ' + reasonBox.val(),
349 function() {
350 doneFn();
351 dialog.dialog( 'close' );
352 that._removeProgressIndicator( dialog );
353 }
354 );
355 };
356
357 var buttons = {};
358 buttons[mw.msg( 'ajax-confirm-save' )] = submitFunction;
359 var dialogOptions = {
360 'AutoOpen' : true,
361 'buttons' : buttons,
362 'width' : 450
363 };
364
365 $( '#catlinks' ).prepend( dialog );
366 dialog.dialog( dialogOptions );
367
368 // Close on enter
369 dialog.keyup( function( e ) {
370 if ( e.keyCode == 13 ) submitFunction();
371 });
372 };
373
374 /**
375 * When multiEdit mode is enabled,
376 * this is called when the user clicks "save all"
377 * Combines the summaries and edit functions
378 */
379 this._handleStashedCategories = function() {
380 var summary = '', fns = _stash.fns;
381
382 // Remove "holes" in array
383 summary = $.grep( _stash.summaries, function( n, i ) {
384 return ( n );
385 });
386 if ( summary.length < 1 ) {
387 // Nothing to do here.
388 _saveAllButton.hide();
389 _cancelAllButton.hide();
390 return;
391 } else {
392 summary = summary.join( '<br>' );
393 }
394 // Remove "holes" in array
395 summaryShort = $.grep( _stash.shortSum, function( n,i ) {
396 return ( n );
397 });
398 summaryShort = summaryShort.join( ', ' );
399
400 var combinedFn = function( oldtext ) {
401 // Run the text through all action functions
402 newtext = oldtext;
403 for ( var i = 0; i < fns.length; i++ ) {
404 if ( $.isFunction( fns[i] ) ) {
405 newtext = fns[i]( newtext );
406 if ( newtext === false ) {
407 return false;
408 }
409 }
410 }
411 return newtext;
412 };
413 var doneFn = function() { that.resetAll( true ); };
414
415 that._confirmEdit( combinedFn, summary, summaryShort, doneFn, '', 'all' );
416 };
417
418 /**
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
424 * @param str summary
425 * @param str doneFn Callback after all is done
426 */
427 this._doEdit = function( page, fn, summary, doneFn ) {
428 // Get an edit token for the page.
429 var getTokenVars = {
430 'action':'query',
431 'prop':'info|revisions',
432 'intoken':'edit',
433 'titles':page,
434 'rvprop':'content|timestamp',
435 'format':'json'
436 };
437
438 $.post( mw.util.wikiScript( 'api' ), getTokenVars,
439 function( reply ) {
440 var infos = reply.query.pages;
441 $.each(
442 infos,
443 function( pageid, data ) {
444 var token = data.edittoken;
445 var timestamp = data.revisions[0].timestamp;
446 var oldText = data.revisions[0]['*'];
447
448 // Replace all nowiki and comments with unique keys
449 var key = mw.user.generateId();
450 var nowiki = [];
451 oldText = replaceNowikis( oldText, key, nowiki );
452
453 // Then do the changes
454 var newText = fn( oldText );
455 if ( newText === false ) return;
456
457 // And restore them back
458 newText = restoreNowikis( newText, key, nowiki );
459
460 var postEditVars = {
461 'action':'edit',
462 'title':page,
463 'text':newText,
464 'summary':summary,
465 'token':token,
466 'basetimestamp':timestamp,
467 'format':'json'
468 };
469
470 $.post( mw.util.wikiScript( 'api' ), postEditVars, doneFn, 'json' )
471 .error( function( xhr, text, error ) {
472 _showError( mw.msg( 'ajax-api-error', text, error ) );
473 });
474 }
475 );
476 }
477 , 'json' ).error( function( xhr, text, error ) {
478 _showError( mw.msg( 'ajax-api-error', text, error ) );
479 });
480 };
481 /**
482 * Append spinner wheel to element
483 * @param DOMObject element.
484 */
485 this._addProgressIndicator = function( elem ) {
486 elem.append( $( '<div/>' ).addClass( 'mw-ajax-loader' ) );
487 };
488
489 /**
490 * Find and remove spinner wheel from inside element
491 * @param DOMObject parent element.
492 */
493 this._removeProgressIndicator = function( elem ) {
494 elem.find( '.mw-ajax-loader' ).remove();
495 };
496
497 /**
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
502 */
503 this._resolveRedirects = function( category, callback ) {
504 if ( !options.resolveRedirects ) {
505 callback( category );
506 return;
507 }
508 var queryVars = {
509 'action':'query',
510 'titles': new mw.Title( category, catNsId ).toString(),
511 'redirects':'',
512 'format' : 'json'
513 };
514
515 $.get( mw.util.wikiScript( 'api' ), queryVars,
516 function( reply ) {
517 var redirect = reply.query.redirects;
518 if ( redirect ) {
519 category = new mw.Title( redirect[0].to )._name;
520 }
521 callback( category, !reply.query.pages[-1] );
522 }
523 , 'json' );
524 };
525
526 /**
527 * Handle add category submit. Not to be called directly
528 */
529 this._handleAddLink = function( e ) {
530 var $this = $( this ), $link = $();
531
532 // Grab category text
533 var category = $this.parent().find( '.mw-addcategory-input' ).val();
534 category = $.ucFirst( category );
535
536 // Resolve redirects
537 that._resolveRedirects( category, function( resolvedCat, exists ) {
538 that.handleCategoryAdd( $link, resolvedCat, false, exists );
539 } );
540 };
541 /**
542 * Execute or queue an category add
543 */
544 this.handleCategoryAdd = function( $link, category, noAppend, exists ) {
545 if ( !$link.length ) {
546 $link = that.createCatLink( category );
547 }
548 // Mark red if missing
549 $link.toggleClass( 'new', exists === false );
550
551 // Handle sortkey
552 var arr = category.split( '|' ), sortkey = '';
553
554 if ( arr.length > 1 ) {
555 category = arr.shift();
556 sortkey = '|' + arr.join( '|' );
557 if ( sortkey == '|' ) sortkey = '';
558 }
559
560 //Replace underscores
561 category = category.replace(/_/g, ' ' );
562
563 if ( that.containsCat( category ) ) {
564 _showError( mw.msg( 'ajax-category-already-present', category ) );
565 return;
566 }
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 + ']]';
571 that._confirmEdit(
572 function( oldText ) {
573 newText = _runHooks ( oldText, 'beforeAdd', category );
574 newText = newText + appendText;
575 return _runHooks ( newText, 'afterAdd', category );
576 },
577 summary,
578 shortSummary,
579 function( unsaved ) {
580 if ( !noAppend ) {
581 options.$container.find( '#mw-normal-catlinks>.mw-addcategory-prompt' ).children( 'input' ).hide();
582 options.$container.find( '#mw-normal-catlinks ul' ).append( $link.parent() );
583 } else {
584 // Remove input box & button
585 $link.data( 'deleteButton' ).click();
586
587 // Update link text and href
588 $link.show().text( category ).attr( 'href', catUrl( category ) );
589 }
590 if ( unsaved ) {
591 $link.addClass( 'mw-added-category' );
592 }
593 $( '.mw-ajax-addcategory' ).click();
594 },
595 $link,
596 'add'
597 );
598 };
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' )
606 );
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() {
611 $input.remove();
612 $link.show();
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' ));
617 };
618
619 /**
620 * Handle edit category submit. Not to be called directly
621 */
622 this._handleEditLink = function( e ) {
623 var $this = $( this ),
624 $link = $this.parent().parent().find( 'a:not(.icon)' ),
625 categoryNew, sortkey = '';
626
627 // Grab category text
628 categoryNew = $this.parent().find( '.mw-addcategory-input' ).val();
629 categoryNew = $.ucFirst( categoryNew.replace(/_/g, ' ' ) );
630
631 // Strip sortkey
632 var arr = categoryNew.split( '|' );
633 if ( arr.length > 1 ) {
634 categoryNew = arr.shift();
635 sortkey = '|' + arr.join( '|' );
636 }
637
638 // Grab text
639 var added = $link.hasClass( 'mw-added-category' );
640 that.resetCatLink ( $link );
641 var category = $link.text();
642
643 // Check for dupes ( exluding itself )
644 if ( category != categoryNew && that.containsCat( categoryNew ) ) {
645 $link.data( 'deleteButton' ).click();
646 return;
647 }
648
649 // Resolve redirects
650 that._resolveRedirects( categoryNew, function( resolvedCat, exists ) {
651 that.handleCategoryEdit( $link, category, resolvedCat, sortkey, exists, added );
652 });
653 };
654 /**
655 * Execute or queue an category edit
656 */
657 this.handleCategoryEdit = function( $link, category, categoryNew, sortkeyNew, exists, added ) {
658 // Category add needs to be handled differently
659 if ( added ) {
660 // Pass sortkey back
661 that.handleCategoryAdd( $link, categoryNew + sortkeyNew, true );
662 return;
663 }
664 // User didn't change anything.
665 if ( category == categoryNew + sortkeyNew ) {
666 $link.data( 'deleteButton' ).click();
667 return;
668 }
669 // Mark red if missing
670 $link.toggleClass( 'new', exists === false );
671
672 categoryRegex = buildRegex( category );
673
674 var summary = mw.msg( 'ajax-edit-category-summary', category, categoryNew );
675 var shortSummary = '[[' + new mw.Title( category, catNsId ) + ']] -> [[' + new mw.Title( categoryNew, catNsId ) + ']]';
676 that._confirmEdit(
677 function( oldText ) {
678 newText = _runHooks ( oldText, 'beforeChange', category, categoryNew );
679
680 var matches = newText.match( categoryRegex );
681
682 //Old cat wasn't found, likely to be transcluded
683 if ( !$.isArray( matches ) ) {
684 _showError( mw.msg( 'ajax-edit-category-error' ) );
685 return false;
686 }
687 var sortkey = sortkeyNew || matches[0].replace( categoryRegex, '$2' );
688 var newCategoryString = "[[" + new mw.Title( categoryNew, catNsId ) + sortkey + ']]';
689
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], '' );
695 }
696 }
697 var newText = oldText.replace( categoryRegex, newCategoryString );
698
699 return _runHooks ( newText, 'afterChange', category, categoryNew );
700 },
701 summary,
702 shortSummary,
703 function( unsaved ) {
704 // Remove input box & button
705 $link.data( 'deleteButton' ).click();
706
707 // Update link text and href
708 $link.show().text( categoryNew ).attr( 'href', catUrl( categoryNew ) );
709 if ( unsaved ) {
710 $link.data( 'origCat', category ).addClass( 'mw-changed-category' );
711 }
712 },
713 $link,
714 'edit'
715 );
716 };
717
718 /**
719 * Handle delete category submit. Not to be called directly
720 */
721 this._handleDeleteLink = function() {
722 var $this = $( this ),
723 $link = $this.parent().find( 'a:not(.icon)' ),
724 category = $link.text();
725
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' ) );
729 return;
730 } else if ( $link.is( '.mw-removed-category' ) ) {
731 // It's already removed...
732 return;
733 }
734 that.handleCategoryDelete( $link, category );
735 };
736
737 /**
738 * Execute or queue an category delete
739 */
740 this.handleCategoryDelete = function( $link, category ) {
741 var categoryRegex = buildRegex( category, true );
742
743 var summary = mw.msg( 'ajax-remove-category-summary', category );
744 var shortSummary = '-[[' + new mw.Title( category, catNsId ) + ']]';
745
746 that._confirmEdit(
747 function( oldText ) {
748 newText = _runHooks ( oldText, 'beforeDelete', category );
749 var newText = newText.replace( categoryRegex, '' );
750
751 if ( newText == oldText ) {
752 _showError( mw.msg( 'ajax-remove-category-error' ) );
753 return false;
754 }
755
756 return _runHooks ( newText, 'afterDelete', category );
757 },
758 summary,
759 shortSummary,
760 function( unsaved ) {
761 if ( unsaved ) {
762 $link.addClass( 'mw-removed-category' );
763 } else {
764 $link.parent().remove();
765 }
766 },
767 $link,
768 'delete'
769 );
770 };
771
772
773 /**
774 * Open a dismissable error dialog
775 *
776 * @param string str The error description
777 */
778 _showError = function( str ) {
779 var oldDialog = $( '.mw-ajax-confirm-dialog' );
780 that._removeProgressIndicator( oldDialog );
781 oldDialog.dialog( 'close' );
782
783 var dialog = $( '<div/>' );
784 dialog.text( str );
785
786 mw.util.$content.append( dialog );
787
788 var buttons = { };
789 buttons[mw.msg( 'ajax-confirm-ok' )] = function( e ) {
790 dialog.dialog( 'close' );
791 };
792 var dialogOptions = {
793 'buttons' : buttons,
794 'AutoOpen' : true,
795 'title' : mw.msg( 'ajax-error-title' )
796 };
797
798 dialog.dialog( dialogOptions );
799
800 // Close on enter
801 dialog.keyup( function( e ) {
802 if ( e.keyCode == 13 ) dialog.dialog( 'close' );
803 });
804 };
805
806 /**
807 * Manufacture iconed button, with or without text
808 *
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.
813 *
814 * @return jQueryObject The button
815 */
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( '&#8203;' );
820
821 if ( text ) {
822 var $icon = $( '<span>' ).addClass( 'icon ' + icon ).html( '&#8203;' );
823 $button.addClass( 'icon-parent' ).append( $icon ).append( text );
824 } else {
825 $button.addClass( 'icon ' + icon );
826 }
827 return $button;
828 };
829
830 /**
831 * Append edit and remove buttons to a given category link
832 *
833 * @param DOMElement element Anchor element, to which the buttons should be appended.
834 */
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' ) );
839
840 //Not yet used
841 var saveButton = _createButton( 'icon-tick', mw.msg( 'ajax-confirm-save' ) ).hide();
842
843 deleteButton.click( that._handleDeleteLink );
844 editButton.click( that._createEditInterface );
845
846 $element.after( deleteButton ).after( editButton );
847
848 //Save references to all links and buttons
849 $element.data({
850 saveButton : saveButton,
851 deleteButton: deleteButton,
852 editButton : editButton
853 });
854 editButton.data({
855 link : $element
856 });
857 };
858
859 /**
860 * Create the UI
861 */
862 this.setup = function() {
863 // Could be set by gadgets like HotCat etc.
864 if ( mw.config.get( 'disableAJAXCategories' ) ) {
865 return false;
866 }
867 // Only do it for articles.
868 if ( !mw.config.get( 'wgIsArticle' ) ) return;
869
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' )
875 );
876 addLink.click( function() {
877 $( this ).nextAll().toggle().filter( '.mw-addcategory-input' ).focus();
878 });
879
880
881 // Create add category prompt
882 _addContainer = that._makeSuggestionBox( '', that._handleAddLink, mw.msg( 'ajax-add-category-submit' ) );
883 _addContainer.children().hide();
884
885 _addContainer.prepend( addLink );
886
887 // Create edit & delete link for each category.
888 $( '#catlinks li a' ).each( function() {
889 _createCatButtons( $( this ) );
890 });
891
892 options.$containerNormal.append( _addContainer );
893
894 //TODO Make more clickable
895 _saveAllButton = _createButton( 'icon-tick',
896 mw.msg( 'ajax-confirm-save-all' ),
897 '',
898 mw.msg( 'ajax-confirm-save-all' )
899 );
900 _cancelAllButton = _createButton( 'icon-close',
901 mw.msg( 'ajax-cancel-all' ),
902 '',
903 mw.msg( 'ajax-cancel-all' )
904 );
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 );
909 };
910
911 _stash = {
912 summaries : [],
913 shortSum : [],
914 fns : []
915 };
916 _removeStashItem = function( i ) {
917 if ( typeof i != "number" ) {
918 i = i.data( 'stashIndex' );
919 }
920 delete _stash.fns[i];
921 delete _stash.summaries[i];
922 if ( $.isEmpty( _stash.fns ) ) {
923 _stash.fns = [];
924 _stash.summaries = [];
925 _stash.shortSum = [];
926 _saveAllButton.hide();
927 _cancelAllButton.hide();
928 }
929 };
930 _hooks = {
931 beforeAdd : [],
932 beforeChange : [],
933 beforeDelete : [],
934 afterAdd : [],
935 afterChange : [],
936 afterDelete : []
937 };
938 _runHooks = function( oldtext, type, category, categoryNew ) {
939 // No hooks registered
940 if ( !_hooks[type] ) {
941 return oldtext;
942 } else {
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 ) );
947 return;
948 }
949 }
950 return oldtext;
951 }
952 };
953 /**
954 * Add hooks
955 * Currently available: beforeAdd, beforeChange, beforeDelete,
956 * afterAdd, afterChange, afterDelete
957 * If the hook function returns false, all changes are aborted.
958 *
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
964 */
965 this.addHook = function( type, fn ) {
966 if ( !_hooks[type] || !$.isFunction( fn ) ) {
967 return;
968 }
969 else {
970 hooks[type].push( fn );
971 }
972 };
973 };
974
975 } )( jQuery );