Rewrite ajaxCategories for ResourceLoader. Add some missing functionality (edit categ...
[lhc/web/wiklou.git] / resources / mediawiki.page / mediawiki.page.ajaxCategories.js
1 // TODO
2 //
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 // * api.php / api.php5
11 // * Simple / MultiEditMode
12
13 ( function( $, mw ) {
14 var catLinkWrapper = '<li/>'
15 var $container = $( '.catlinks' );
16
17 var categoryLinkSelector = '#mw-normal-catlinks li a';
18 var _request;
19
20 var _catElements = {};
21 var _otherElements = {};
22
23 var namespaceIds = mw.config.get( 'wgNamespaceIds' )
24 var categoryNamespaceId = namespaceIds['category'];
25 var categoryNamespace = mw.config.get( 'wgFormattedNamespaces' )[categoryNamespaceId];
26 var wgScriptPath = mw.config.get( 'wgScriptPath' );
27
28 function _fetchSuggestions ( query ) {
29 //SYNCED
30 var _this = this;
31 // ignore bad characters, they will be stripped out
32 var catName = _stripIllegals( $( this ).val() );
33 var request = $.ajax( {
34 url: wgScriptPath + '/api.php',
35 data: {
36 'action': 'query',
37 'list': 'allpages',
38 'apnamespace': categoryNamespaceId,
39 'apprefix': catName,
40 'format': 'json'
41 },
42 dataType: 'json',
43 success: function( data ) {
44 // Process data.query.allpages into an array of titles
45 var pages = data.query.allpages;
46 var titleArr = [];
47
48 $.each( pages, function( i, page ) {
49 var title = page.title.split( ':', 2 )[1];
50 titleArr.push( title );
51 } );
52
53 $( _this ).suggestions( 'suggestions', titleArr );
54 }
55 } );
56 //TODO
57 _request = request;
58 }
59
60 function _stripIllegals( cat ) {
61 return cat.replace( /[\x00-\x1f\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f]+/g, '' );
62 }
63
64 function _insertCatDOM( cat, isHidden ) {
65 // User can implicitely state a sort key.
66 // Remove before display
67 cat = cat.replace(/\|.*/, '');
68
69 // strip out bad characters
70 cat = _stripIllegals ( cat );
71
72 if ( $.isEmpty( cat ) || _containsCat( cat ) ) {
73 return;
74 }
75
76 var $catLinkWrapper = $( catLinkWrapper );
77 var $anchor = $( '<a/>' ).append( cat );
78 $catLinkWrapper.append( $anchor );
79 $anchor.attr( { target: "_blank", href: _catLink( cat ) } );
80 if ( isHidden ) {
81 $container.find( '#mw-hidden-catlinks ul' ).append( $catLinkWrapper );
82 } else {
83 $container.find( '#mw-normal-catlinks ul' ).append( $catLinkWrapper );
84 }
85 _createCatButtons( $anchor.get(0) );
86 }
87
88 function _makeSuggestionBox( prefill, callback, buttonVal ) {
89 // Create add category prompt
90 var promptContainer = $( '<div class="mw-addcategory-prompt"/>' );
91 var promptTextbox = $( '<input type="text" size="45" class="mw-addcategory-input"/>' );
92 if ( prefill !== '' ) {
93 promptTextbox.val( prefill );
94 }
95 var addButton = $( '<input type="button" class="mw-addcategory-button"/>' );
96 addButton.val( buttonVal );
97
98 addButton.click( callback );
99
100 promptTextbox.suggestions( {
101 'fetch':_fetchSuggestions,
102 'cancel': function() {
103 var req = _request;
104 // XMLHttpRequest.abort is unimplemented in IE6, also returns nonstandard value of "unknown" for typeof
105 if ( req && ( typeof req.abort !== 'unknown' ) && ( typeof req.abort !== 'undefined' ) && req.abort ) {
106 req.abort();
107 }
108 }
109 } );
110
111 promptTextbox.suggestions();
112
113 promptContainer.append( promptTextbox );
114 promptContainer.append( addButton );
115
116 return promptContainer;
117 }
118
119 // Create a valid link to the category.
120 function _catLink ( cat ) {
121 //SYNCED
122 return mw.util.wikiGetlink( categoryNamespace + ':' + $.ucFirst( cat ) );
123 }
124
125 function _getCats() {
126 return $container.find( categoryLinkSelector ).map( function() { return $.trim( $( this ).text() ); } );
127 }
128
129 function _containsCat( cat ) {
130 //TODO: SYNC
131 return _getCats().filter( function() { return $.ucFirst(this) == $.ucFirst(cat); } ).length !== 0;
132 }
133
134 function _confirmEdit ( page, fn, actionSummary, doneFn ) {
135
136 // Produce a confirmation dialog
137 var dialog = $( '<div/>' );
138
139 dialog.addClass( 'mw-ajax-confirm-dialog' );
140 dialog.attr( 'title', mw.msg( 'ajax-confirm-title' ) );
141
142 // Intro text.
143 var confirmIntro = $( '<p/>' );
144 confirmIntro.text( mw.msg( 'ajax-confirm-prompt' ) );
145 dialog.append( confirmIntro );
146
147 // Summary of the action to be taken
148 var summaryHolder = $( '<p/>' );
149 var summaryLabel = $( '<strong/>' );
150 summaryLabel.text( mw.msg( 'ajax-confirm-actionsummary' ) + " " );
151 summaryHolder.text( actionSummary );
152 summaryHolder.prepend( summaryLabel );
153 dialog.append( summaryHolder );
154
155 // Reason textbox.
156 var reasonBox = $( '<input type="text" size="45" />' );
157 reasonBox.addClass( 'mw-ajax-confirm-reason' );
158 dialog.append( reasonBox );
159
160 // Submit button
161 var submitButton = $( '<input type="button"/>' );
162 submitButton.val( mw.msg( 'ajax-confirm-save' ) );
163
164 var submitFunction = function() {
165 _addProgressIndicator( dialog );
166 _doEdit(
167 page,
168 fn,
169 reasonBox.val(),
170 function() {
171 doneFn();
172 dialog.dialog( 'close' );
173 _removeProgressIndicator( dialog );
174 }
175 );
176 };
177
178 var buttons = { };
179 buttons[mw.msg( 'ajax-confirm-save' )] = submitFunction;
180 var dialogOptions = {
181 'AutoOpen' : true,
182 'buttons' : buttons,
183 'width' : 450
184 };
185
186 $( '#catlinks' ).prepend( dialog );
187 dialog.dialog( dialogOptions );
188 }
189
190 function _doEdit ( page, fn, summary, doneFn ) {
191 // Get an edit token for the page.
192 var getTokenVars = {
193 'action':'query',
194 'prop':'info|revisions',
195 'intoken':'edit',
196 'titles':page,
197 'rvprop':'content|timestamp',
198 'format':'json'
199 };
200
201 $.get( wgScriptPath + '/api.php', getTokenVars,
202 function( reply ) {
203 var infos = reply.query.pages;
204 $.each(
205 infos,
206 function( pageid, data ) {
207 var token = data.edittoken;
208 var timestamp = data.revisions[0].timestamp;
209 var oldText = data.revisions[0]['*'];
210
211 var newText = fn( oldText );
212
213 if ( newText === false ) return;
214
215 var postEditVars = {
216 'action':'edit',
217 'title':page,
218 'text':newText,
219 'summary':summary,
220 'token':token,
221 'basetimestamp':timestamp,
222 'format':'json'
223 };
224
225 $.post( wgScriptPath + '/api.php', postEditVars, doneFn, 'json' );
226 }
227 );
228 }
229 , 'json' );
230 }
231
232 function _addProgressIndicator ( elem ) {
233 var indicator = $( '<div/>' );
234
235 indicator.addClass( 'mw-ajax-loader' );
236
237 elem.append( indicator );
238 }
239
240 function _removeProgressIndicator ( elem ) {
241 elem.find( '.mw-ajax-loader' ).remove();
242 }
243
244 function _makeCaseInsensitiv( string ) {
245 var newString = '';
246 for (var i=0; i < string.length; i++) {
247 newString += '[' + string[i].toUpperCase() + string[i].toLowerCase() + ']';
248 };
249 return newString;
250 }
251 function _buildRegex ( category ) {
252 // Build a regex that matches legal invocations of that category.
253 var categoryNSFragment = '';
254 $.each( namespaceIds, function( name, id ) {
255 if ( id == 14 ) {
256 // The parser accepts stuff like cATegORy,
257 // we need to do the same
258 categoryNSFragment += '|' + _makeCaseInsensitiv ( $.escapeRE(name) );
259 }
260 } );
261 categoryNSFragment = categoryNSFragment.substr( 1 ); // Remove leading |
262
263 // Build the regex
264 var titleFragment = $.escapeRE(category);
265
266 firstChar = category.charAt( 0 );
267 firstChar = '[' + firstChar.toUpperCase() + firstChar.toLowerCase() + ']';
268 titleFragment = firstChar + category.substr( 1 );
269 var categoryRegex = '\\[\\[(' + categoryNSFragment + '):' + titleFragment + '(\\|[^\\]]*)?\\]\\]';
270
271 return new RegExp( categoryRegex, 'g' );
272 }
273
274 function _handleEditLink ( e ) {
275 e.preventDefault();
276 var $this = $( this );
277 var $link = $this.parent().find( 'a:not(.icon)' );
278 var category = $link.text();
279
280 var $input = _makeSuggestionBox( category, _handleCategoryEdit, mw.msg( 'ajax-confirm-save' ) );
281 $link.after( $input ).hide();
282 _catElements[category].editButton.hide();
283 _catElements[category].deleteButton.unbind('click').click( function() {
284 $input.remove();
285 $link.show();
286 _catElements[category].editButton.show();
287 $( this ).unbind('click').click( _handleDeleteLink );
288 });
289 }
290
291 function _handleAddLink ( e ) {
292 e.preventDefault();
293
294 $container.find( '#mw-normal-catlinks>.mw-addcategory-prompt' ).toggle();
295 }
296
297 function _handleDeleteLink ( e ) {
298 e.preventDefault();
299
300 var $this = $( this );
301 var $link = $this.parent().find( 'a:not(.icon)' );
302 var category = $link.text();
303
304 categoryRegex = _buildRegex( category );
305
306 var summary = mw.msg( 'ajax-remove-category-summary', category );
307
308 _confirmEdit(
309 mw.config.get('wgPageName'),
310 function( oldText ) {
311 //TODO Cleanup whitespace safely?
312 var newText = oldText.replace( categoryRegex, '' );
313
314 if ( newText == oldText ) {
315 var error = mw.msg( 'ajax-remove-category-error' );
316 _showError( error );
317 _removeProgressIndicator( $( '.mw-ajax-confirm-dialog' ) );
318 $( '.mw-ajax-confirm-dialog' ).dialog( 'close' );
319 return false;
320 }
321
322 return newText;
323 },
324 summary,
325 function() {
326 $this.parent().remove();
327 }
328 );
329 }
330
331 function _handleCategoryAdd ( e ) {
332 // Grab category text
333 var category = $( this ).parent().find( '.mw-addcategory-input' ).val();
334 category = $.ucFirst( category );
335
336 if ( _containsCat(category) ) {
337 // TODO add info alert
338 return;
339 }
340 var appendText = "\n[[" + categoryNamespace + ":" + category + "]]\n";
341 var summary = mw.msg( 'ajax-add-category-summary', category );
342
343 _confirmEdit(
344 mw.config.get( 'wgPageName' ),
345 function( oldText ) { return oldText + appendText },
346 summary,
347 function() {
348 _insertCatDOM( category, false );
349 }
350 );
351 }
352
353 function _handleCategoryEdit ( e ) {
354 e.preventDefault();
355
356 // Grab category text
357 var categoryNew = $( this ).parent().find( '.mw-addcategory-input' ).val();
358 categoryNew = $.ucFirst( categoryNew );
359
360 var $this = $( this );
361 var $link = $this.parent().parent().find( 'a:not(.icon)' );
362 var category = $link.text();
363
364 // User didn't change anything. Just close the box
365 if ( category == categoryNew ) {
366 $this.parent().remove();
367 $link.show();
368 return;
369 }
370 categoryRegex = _buildRegex( category );
371
372 var summary = mw.msg( 'ajax-edit-category-summary', category, categoryNew );
373
374 _confirmEdit(
375 mw.config.get( 'wgPageName' ),
376 function( oldText ) {
377 var matches = oldText.match( categoryRegex );
378
379 //Old cat wasn't found, likely to be transcluded
380 if ( !$.isArray( matches ) ) {
381 var error = mw.msg( 'ajax-edit-category-error' );
382 _showError( error );
383 _removeProgressIndicator( $( '.mw-ajax-confirm-dialog' ) );
384 $( '.mw-ajax-confirm-dialog' ).dialog( 'close' );
385 return false;
386 }
387 var sortkey = matches[0].replace( categoryRegex, '$2' );
388 var newCategoryString = "[[" + categoryNamespace + ":" + categoryNew + sortkey + ']]';
389
390 if (matches.length > 1) {
391 // The category is duplicated.
392 // Remove all but one match
393 for (var i = 1; i < matches.length; i++) {
394 oldText = oldText.replace( matches[i], '');
395 }
396 }
397 var newText = oldText.replace( categoryRegex, newCategoryString );
398
399 return newText;
400 },
401 summary,
402 function() {
403 // Remove input box & button
404 $this.parent().remove();
405
406 // Update link text and href
407 $link.show().text( categoryNew ).attr( 'href', _catLink( categoryNew ) );
408 }
409 );
410 }
411 function _showError ( str ) {
412 var dialog = $( '<div/>' );
413 dialog.text( str );
414
415 $( '#bodyContent' ).append( dialog );
416
417 var buttons = { };
418 buttons[mw.msg( 'ajax-error-dismiss' )] = function( e ) {
419 dialog.dialog( 'close' );
420 };
421 var dialogOptions = {
422 'buttons' : buttons,
423 'AutoOpen' : true,
424 'title' : mw.msg( 'ajax-error-title' )
425 };
426
427 dialog.dialog( dialogOptions );
428 }
429
430 function _createButton ( icon, title, category, text ){
431 var button = $( '<a>' ).addClass( category || '' )
432 .attr('title', title);
433
434 if ( text ) {
435 var icon = $( '<a>' ).addClass( 'icon ' + icon );
436 button.addClass( 'icon-parent' ).append( icon ).append( text );
437 } else {
438 button.addClass( 'icon ' + icon );
439 }
440 return button;
441 }
442 function _createCatButtons ( element ) {
443 // Create remove & edit buttons
444 var deleteButton = _createButton('icon-close', mw.msg( 'ajax-remove-category' ) );
445 var editButton = _createButton('icon-edit', mw.msg( 'ajax-edit-category' ) );
446
447 //Not yet used
448 var saveButton = _createButton('icon-tick', mw.msg( 'ajax-confirm-save' ) ).hide();
449
450 deleteButton.click( _handleDeleteLink );
451 editButton.click( _handleEditLink );
452
453 $( element ).after( deleteButton ).after( editButton );
454
455 //Save references to all links and buttons
456 _catElements[$( element ).text()] = {
457 link : $( element ),
458 parent : $( element ).parent(),
459 saveButton : saveButton,
460 deleteButton: deleteButton,
461 editButton : editButton
462 };
463 }
464 function _setup() {
465 // Could be set by gadgets like HotCat etc.
466 if ( mw.config.get('disableAJAXCategories') ) {
467 return;
468 }
469 // Only do it for articles.
470 if ( !mw.config.get( 'wgIsArticle' ) ) return;
471
472 var clElement = $( '#mw-normal-catlinks' );
473
474 // Unhide hidden category holders.
475 $('#mw-hidden-catlinks').show();
476
477
478 // Create [Add Category] link
479 var addLink = _createButton('icon-add',
480 mw.msg( 'ajax-add-category' ),
481 'mw-ajax-addcategory',
482 mw.msg( 'ajax-add-category' )
483 );
484 addLink.click( _handleAddLink );
485 clElement.append( addLink );
486
487 // Create add category prompt
488 var promptContainer = _makeSuggestionBox( '', _handleCategoryAdd, mw.msg( 'ajax-add-category-submit' ) );
489 promptContainer.hide();
490
491 // Create edit & delete link for each category.
492 $( '#catlinks li a' ).each( function( e ) {
493 _createCatButtons( this );
494 });
495
496 clElement.append( promptContainer );
497 }
498 function _teardown() {
499
500 }
501 _tasks = {
502 list : [],
503 executed : [],
504 add : function( obj ) {
505 this.list.push( obj );
506 },
507 next : function() {
508 var task = this.list.shift();
509 //run task
510 this.executed.push( task );
511 }
512 }
513 $(document).ready( function() {_setup()});
514
515 } )( jQuery, mediaWiki );