r92112 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r92111‎ | r92112 | r92113 >
Date:22:36, 13 July 2011
Author:diebuche
Status:ok (Comments)
Tags:
Comment:
Rewrite ajaxCategories for ResourceLoader. Add some missing functionality (edit categories and more). Move styles from shared.css into own stylesheet. Fix regex bugs
Modified paths:
  • /trunk/phase3/includes/DefaultSettings.php (modified) (history)
  • /trunk/phase3/includes/OutputPage.php (modified) (history)
  • /trunk/phase3/languages/messages/MessagesEn.php (modified) (history)
  • /trunk/phase3/maintenance/language/messages.inc (modified) (history)
  • /trunk/phase3/resources/Resources.php (modified) (history)
  • /trunk/phase3/resources/mediawiki.page/images (added) (history)
  • /trunk/phase3/resources/mediawiki.page/images/AJAXCategorySprite.png (added) (history)
  • /trunk/phase3/resources/mediawiki.page/mediawiki.page.ajaxCategories.css (added) (history)
  • /trunk/phase3/resources/mediawiki.page/mediawiki.page.ajaxCategories.js (modified) (history)
  • /trunk/phase3/skins/common/shared.css (modified) (history)

Diff [purge]

Index: trunk/phase3/maintenance/language/messages.inc
@@ -3459,6 +3459,24 @@
34603460 'unwatch' => array(
34613461 'confirm-unwatch-button',
34623462 ),
 3463+ 'ajax-category' => array(
 3464+ 'ajax-add-category',
 3465+ 'ajax-remove-category',
 3466+ 'ajax-edit-category',
 3467+ 'ajax-add-category-submit',
 3468+ 'ajax-confirm-title',
 3469+ 'ajax-confirm-prompt',
 3470+ 'ajax-confirm-save',
 3471+ 'ajax-add-category-summary',
 3472+ 'ajax-edit-category-summary',
 3473+ 'ajax-remove-category-summary',
 3474+ 'ajax-confirm-actionsummary',
 3475+ 'ajax-error-title',
 3476+ 'ajax-error-dismiss',
 3477+ 'ajax-remove-category-error',
 3478+ 'ajax-edit-category-error',
 3479+ ),
 3480+
34633481 );
34643482
34653483 /** Comments for each block */
@@ -3687,4 +3705,5 @@
36883706 'db-error-messages' => 'Database error messages',
36893707 'html-forms' => 'HTML forms',
36903708 'sqlite' => 'SQLite database support',
 3709+ 'ajax-category' => 'Add categories per AJAX',
36913710 );
Index: trunk/phase3/skins/common/shared.css
@@ -583,32 +583,6 @@
584584 word-wrap: break-word;
585585 }
586586
587 -#mw-addcategory-prompt {
588 - display: inline;
589 - margin-left: 1em;
590 -}
591 -
592 -#mw-addcategory-prompt input {
593 - margin-left: 0.5em;
594 - margin-right: 0.5em;
595 -}
596 -
597 -.mw-remove-category {
598 - padding: 8px;
599 - /* @embed */
600 - background-image: url(images/remove.png);
601 - background-position: center center;
602 - background-repeat: no-repeat;
603 -}
604 -
605 -.mw-ajax-addcategory {
606 - padding-left: 20px;
607 - /* @embed */
608 - background-image: url(images/add.png);
609 - background-position: left center;
610 - background-repeat: no-repeat;
611 -}
612 -
613587 .mw-ajax-loader {
614588 /* @embed */
615589 background-image: url(images/ajax-loader.gif);
Index: trunk/phase3/includes/OutputPage.php
@@ -2340,8 +2340,8 @@
23412341 * Add the default ResourceLoader modules to this object
23422342 */
23432343 private function addDefaultModules() {
2344 - global $wgIncludeLegacyJavaScript,
2345 - $wgUseAjax, $wgAjaxWatch, $wgEnableMWSuggest;
 2344+ global $wgIncludeLegacyJavaScript, $wgUseAjax,
 2345+ $wgAjaxWatch, $wgEnableMWSuggest, $wgUseAJAXCategories;
23462346
23472347 // Add base resources
23482348 $this->addModules( array(
@@ -2369,9 +2369,19 @@
23702370 }
23712371 }
23722372
2373 - if( $this->getUser()->getBoolOption( 'editsectiononrightclick' ) ) {
 2373+ if ( $this->getUser()->getBoolOption( 'editsectiononrightclick' ) ) {
23742374 $this->addModules( 'mediawiki.action.view.rightClickEdit' );
23752375 }
 2376+
 2377+ if ( $wgUseAJAXCategories ) {
 2378+ global $wgAJAXCategoriesNamespaces;
 2379+
 2380+ $title = $this->getTitle();
 2381+
 2382+ if( empty( $wgAJAXCategoriesNamespaces ) || in_array( $title->getNamespace(), $wgAJAXCategoriesNamespaces ) ) {
 2383+ $this->addModules( 'mediawiki.page.ajaxCategories' );
 2384+ }
 2385+ }
23762386 }
23772387
23782388 /**
Index: trunk/phase3/includes/DefaultSettings.php
@@ -5501,6 +5501,19 @@
55025502 $wgDBtestpassword = '';
55035503
55045504 /**
 5505+ * Whether or not to use the AJAX categories system.
 5506+ */
 5507+$wgUseAJAXCategories = false;
 5508+
 5509+/**
 5510+ * Only enable AJAXCategories on configured namespaces. Default is all.
 5511+ *
 5512+ * Example:
 5513+ * $wgAJAXCategoriesNamespaces = array( NS_MAIN, NS_PROJECT );
 5514+ */
 5515+$wgAJAXCategoriesNamespaces = array();
 5516+
 5517+/**
55055518 * For really cool vim folding this needs to be at the end:
55065519 * vim: foldmarker=@{,@} foldmethod=marker
55075520 * @}
Index: trunk/phase3/languages/messages/MessagesEn.php
@@ -4586,4 +4586,24 @@
45874587 'sqlite-has-fts' => '$1 with full-text search support',
45884588 'sqlite-no-fts' => '$1 without full-text search support',
45894589
 4590+
 4591+# Add categories per AJAX
 4592+'ajax-add-category' => 'Add category',
 4593+'ajax-remove-category' => 'Remove category',
 4594+'ajax-edit-category' => 'Edit category',
 4595+'ajax-add-category-submit' => 'Add',
 4596+'ajax-confirm-title' => 'Confirm action',
 4597+'ajax-confirm-prompt' => 'You can provide an edit summary below.
 4598+Click "Save" to save your edit.',
 4599+'ajax-confirm-save' => 'Save',
 4600+'ajax-add-category-summary' => 'Add category "$1"',
 4601+'ajax-edit-category-summary' => 'Change category "$1" to "$2"',
 4602+'ajax-remove-category-summary' => 'Remove category "$1"',
 4603+'ajax-confirm-actionsummary' => 'Action to take:',
 4604+'ajax-error-title' => 'Error',
 4605+'ajax-error-dismiss' => 'OK',
 4606+'ajax-remove-category-error' => 'It was not possible to remove this category.
 4607+This usually occurs when the category has been added to the page in a template.',
 4608+'ajax-edit-category-error' => 'It was not possible to edit this category.
 4609+This usually occurs when the category has been added to the page in a template.',
45904610 );
Index: trunk/phase3/resources/Resources.php
@@ -483,6 +483,31 @@
484484 'jquery.ui.autocomplete',
485485 ),
486486 ),
 487+ 'mediawiki.page.ajaxCategories' => array(
 488+ 'scripts' => 'resources/mediawiki.page/mediawiki.page.ajaxCategories.js',
 489+ 'styles' => 'resources/mediawiki.page/mediawiki.page.ajaxCategories.css',
 490+ 'dependencies' => array(
 491+ 'jquery.suggestions',
 492+ 'jquery.ui.dialog',
 493+ ),
 494+ 'messages' => array(
 495+ 'ajax-add-category',
 496+ 'ajax-remove-category',
 497+ 'ajax-edit-category',
 498+ 'ajax-add-category-submit',
 499+ 'ajax-confirm-prompt',
 500+ 'ajax-confirm-title',
 501+ 'ajax-confirm-save',
 502+ 'ajax-add-category-summary',
 503+ 'ajax-remove-category-summary',
 504+ 'ajax-edit-category-summary',
 505+ 'ajax-confirm-actionsummary',
 506+ 'ajax-error-title',
 507+ 'ajax-error-dismiss',
 508+ 'ajax-remove-category-error',
 509+ 'ajax-edit-category-error',
 510+ ),
 511+ ),
487512 'mediawiki.libs.jpegmeta' => array(
488513 'scripts' => 'resources/mediawiki.libs/mediawiki.libs.jpegmeta.js',
489514 ),
Index: trunk/phase3/resources/mediawiki.page/mediawiki.page.ajaxCategories.js
@@ -1,50 +1,42 @@
2 -loadGM( {
3 - "ajax-add-category" : "[Add Category]",
4 - "ajax-add-category-submit" : "[Add]",
5 - "ajax-confirm-prompt" : "[Confirmation Text]",
6 - "ajax-confirm-title" : "[Confirmation Title]",
7 - "ajax-confirm-save" : "[Save]",
8 - "ajax-add-category-summary" : "[Add category $1]",
9 - "ajax-remove-category-summary" : "[Remove category $2]",
10 - "ajax-confirm-actionsummary" : "[Summary]",
11 - "ajax-error-title" : "Error",
12 - "ajax-error-dismiss" : "OK",
13 - "ajax-remove-category-error" : "[RemoveErr]"
14 -} );
 2+// TODO
 3+//
 4+// * The edit summary should contain the added/removed category name too.
 5+// Something like: "Category:Foo added. Reason"
 6+// Requirement: Be able to get msg with lang option.
 7+// * Handle uneditable cats. Needs serverside changes!
 8+// * Add Hooks for change, delete, add
 9+// * Add Hooks for soft redirect
 10+// * Handle normal redirects
 11+// * api.php / api.php5
 12+// * Simple / MultiEditMode
1513
16 -var ajaxCategories = {
17 - handleAddLink : function( e ) {
18 - e.preventDefault();
 14+( function( $, mw ) {
 15+ var catLinkWrapper = '<li/>'
 16+ var $container = $( '.catlinks' );
 17+
 18+ var categoryLinkSelector = '#mw-normal-catlinks li a';
 19+ var _request;
 20+
 21+ var _catElements = {};
 22+ var _otherElements = {};
1923
20 - // Make sure the suggestion plugin is loaded. Load everything else while we're at it
21 - mvJsLoader.doLoad(
22 - ['$j.ui', '$j.ui.dialog', '$j.fn.suggestions'],
23 - function() {
24 - $j( '#mw-addcategory-prompt' ).toggle();
 24+ var namespaceIds = mw.config.get( 'wgNamespaceIds' )
 25+ var categoryNamespaceId = namespaceIds['category'];
 26+ var categoryNamespace = mw.config.get( 'wgFormattedNamespaces' )[categoryNamespaceId];
 27+ var wgScriptPath = mw.config.get( 'wgScriptPath' );
2528
26 - $j( '#mw-addcategory-input' ).suggestions( {
27 - 'fetch':ajaxCategories.fetchSuggestions,
28 - 'cancel': function() {
29 - var req = ajaxCategories.request;
30 - if ( req.abort )
31 - req.abort();
32 - }
33 - } );
34 -
35 - $j( '#mw-addcategory-input' ).suggestions();
36 - }
37 - );
38 - },
39 -
40 - fetchSuggestions : function( query ) {
41 - var that = this;
42 - var request = $j.ajax( {
 29+ function _fetchSuggestions ( query ) {
 30+ //SYNCED
 31+ var _this = this;
 32+ // ignore bad characters, they will be stripped out
 33+ var catName = _stripIllegals( $( this ).val() );
 34+ var request = $.ajax( {
4335 url: wgScriptPath + '/api.php',
4436 data: {
4537 'action': 'query',
4638 'list': 'allpages',
47 - 'apnamespace': 14,
48 - 'apprefix': $j( this ).val(),
 39+ 'apnamespace': categoryNamespaceId,
 40+ 'apprefix': catName,
4941 'format': 'json'
5042 },
5143 dataType: 'json',
@@ -53,94 +45,149 @@
5446 var pages = data.query.allpages;
5547 var titleArr = [];
5648
57 - $j.each( pages, function( i, page ) {
 49+ $.each( pages, function( i, page ) {
5850 var title = page.title.split( ':', 2 )[1];
5951 titleArr.push( title );
6052 } );
6153
62 - $j( that ).suggestions( 'suggestions', titleArr );
 54+ $( _this ).suggestions( 'suggestions', titleArr );
6355 }
6456 } );
 57+ //TODO
 58+ _request = request;
 59+ }
6560
66 - ajaxCategories.request = request;
67 - },
 61+ function _stripIllegals( cat ) {
 62+ return cat.replace( /[\x00-\x1f\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f]+/g, '' );
 63+ }
 64+
 65+ function _insertCatDOM( cat, isHidden ) {
 66+ // User can implicitely state a sort key.
 67+ // Remove before display
 68+ cat = cat.replace(/\|.*/, '');
6869
69 - reloadCategoryList : function( response ) {
70 - var holder = $j( '<div/>' );
 70+ // strip out bad characters
 71+ cat = _stripIllegals ( cat );
7172
72 - holder.load(
73 - window.location.href + ' .catlinks',
74 - function() {
75 - $j( '.catlinks' ).replaceWith( holder.find( '.catlinks' ) );
76 - ajaxCategories.setupAJAXCategories();
77 - ajaxCategories.removeProgressIndicator( $j( '.catlinks' ) );
 73+ if ( $.isEmpty( cat ) || _containsCat( cat ) ) {
 74+ return;
 75+ }
 76+
 77+ var $catLinkWrapper = $( catLinkWrapper );
 78+ var $anchor = $( '<a/>' ).append( cat );
 79+ $catLinkWrapper.append( $anchor );
 80+ $anchor.attr( { target: "_blank", href: _catLink( cat ) } );
 81+ if ( isHidden ) {
 82+ $container.find( '#mw-hidden-catlinks ul' ).append( $catLinkWrapper );
 83+ } else {
 84+ $container.find( '#mw-normal-catlinks ul' ).append( $catLinkWrapper );
 85+ }
 86+ _createCatButtons( $anchor.get(0) );
 87+ }
 88+
 89+ function _makeSuggestionBox( prefill, callback, buttonVal ) {
 90+ // Create add category prompt
 91+ var promptContainer = $( '<div class="mw-addcategory-prompt"/>' );
 92+ var promptTextbox = $( '<input type="text" size="45" class="mw-addcategory-input"/>' );
 93+ if ( prefill !== '' ) {
 94+ promptTextbox.val( prefill );
 95+ }
 96+ var addButton = $( '<input type="button" class="mw-addcategory-button"/>' );
 97+ addButton.val( buttonVal );
 98+
 99+ addButton.click( callback );
 100+
 101+ promptTextbox.suggestions( {
 102+ 'fetch':_fetchSuggestions,
 103+ 'cancel': function() {
 104+ var req = _request;
 105+ // XMLHttpRequest.abort is unimplemented in IE6, also returns nonstandard value of "unknown" for typeof
 106+ if ( req && ( typeof req.abort !== 'unknown' ) && ( typeof req.abort !== 'undefined' ) && req.abort ) {
 107+ req.abort();
 108+ }
78109 }
79 - );
80 - },
 110+ } );
81111
82 - confirmEdit : function( page, fn, actionSummary, doneFn ) {
83 - // Load jQuery UI
84 - mvJsLoader.doLoad(
85 - ['$j.ui', '$j.ui.dialog', '$j.fn.suggestions'],
86 - function() {
87 - // Produce a confirmation dialog
 112+ promptTextbox.suggestions();
88113
89 - var dialog = $j( '<div/>' );
 114+ promptContainer.append( promptTextbox );
 115+ promptContainer.append( addButton );
90116
91 - dialog.addClass( 'mw-ajax-confirm-dialog' );
92 - dialog.attr( 'title', gM( 'ajax-confirm-title' ) );
 117+ return promptContainer;
 118+ }
 119+
 120+ // Create a valid link to the category.
 121+ function _catLink ( cat ) {
 122+ //SYNCED
 123+ return mw.util.wikiGetlink( categoryNamespace + ':' + $.ucFirst( cat ) );
 124+ }
 125+
 126+ function _getCats() {
 127+ return $container.find( categoryLinkSelector ).map( function() { return $.trim( $( this ).text() ); } );
 128+ }
93129
94 - // Intro text.
95 - var confirmIntro = $j( '<p/>' );
96 - confirmIntro.text( gM( 'ajax-confirm-prompt' ) );
97 - dialog.append( confirmIntro );
 130+ function _containsCat( cat ) {
 131+ //TODO: SYNC
 132+ return _getCats().filter( function() { return $.ucFirst(this) == $.ucFirst(cat); } ).length !== 0;
 133+ }
 134+
 135+ function _confirmEdit ( page, fn, actionSummary, doneFn ) {
98136
99 - // Summary of the action to be taken
100 - var summaryHolder = $j( '<p/>' );
101 - var summaryLabel = $j( '<strong/>' );
102 - summaryLabel.text( gM( 'ajax-confirm-actionsummary' ) + " " );
103 - summaryHolder.text( actionSummary );
104 - summaryHolder.prepend( summaryLabel );
105 - dialog.append( summaryHolder );
 137+ // Produce a confirmation dialog
 138+ var dialog = $( '<div/>' );
106139
107 - // Reason textbox.
108 - var reasonBox = $j( '<input type="text" size="45" />' );
109 - reasonBox.addClass( 'mw-ajax-confirm-reason' );
110 - dialog.append( reasonBox );
 140+ dialog.addClass( 'mw-ajax-confirm-dialog' );
 141+ dialog.attr( 'title', mw.msg( 'ajax-confirm-title' ) );
111142
112 - // Submit button
113 - var submitButton = $j( '<input type="button"/>' );
114 - submitButton.val( gM( 'ajax-confirm-save' ) );
 143+ // Intro text.
 144+ var confirmIntro = $( '<p/>' );
 145+ confirmIntro.text( mw.msg( 'ajax-confirm-prompt' ) );
 146+ dialog.append( confirmIntro );
115147
116 - var submitFunction = function() {
117 - ajaxCategories.addProgressIndicator( dialog );
118 - ajaxCategories.doEdit(
119 - page,
120 - fn,
121 - reasonBox.val(),
122 - function() {
123 - doneFn();
124 - dialog.dialog( 'close' );
125 - ajaxCategories.removeProgressIndicator( dialog );
126 - }
127 - );
128 - };
 148+ // Summary of the action to be taken
 149+ var summaryHolder = $( '<p/>' );
 150+ var summaryLabel = $( '<strong/>' );
 151+ summaryLabel.text( mw.msg( 'ajax-confirm-actionsummary' ) + " " );
 152+ summaryHolder.text( actionSummary );
 153+ summaryHolder.prepend( summaryLabel );
 154+ dialog.append( summaryHolder );
129155
130 - var buttons = { };
131 - buttons[gM( 'ajax-confirm-save' )] = submitFunction;
132 - var dialogOptions = {
133 - 'AutoOpen' : true,
134 - 'buttons' : buttons,
135 - 'width' : 450
136 - };
 156+ // Reason textbox.
 157+ var reasonBox = $( '<input type="text" size="45" />' );
 158+ reasonBox.addClass( 'mw-ajax-confirm-reason' );
 159+ dialog.append( reasonBox );
137160
138 - $j( '#catlinks' ).prepend( dialog );
139 - dialog.dialog( dialogOptions );
140 - }
141 - );
142 - },
 161+ // Submit button
 162+ var submitButton = $( '<input type="button"/>' );
 163+ submitButton.val( mw.msg( 'ajax-confirm-save' ) );
143164
144 - doEdit : function( page, fn, summary, doneFn ) {
 165+ var submitFunction = function() {
 166+ _addProgressIndicator( dialog );
 167+ _doEdit(
 168+ page,
 169+ fn,
 170+ reasonBox.val(),
 171+ function() {
 172+ doneFn();
 173+ dialog.dialog( 'close' );
 174+ _removeProgressIndicator( dialog );
 175+ }
 176+ );
 177+ };
 178+
 179+ var buttons = { };
 180+ buttons[mw.msg( 'ajax-confirm-save' )] = submitFunction;
 181+ var dialogOptions = {
 182+ 'AutoOpen' : true,
 183+ 'buttons' : buttons,
 184+ 'width' : 450
 185+ };
 186+
 187+ $( '#catlinks' ).prepend( dialog );
 188+ dialog.dialog( dialogOptions );
 189+ }
 190+
 191+ function _doEdit ( page, fn, summary, doneFn ) {
145192 // Get an edit token for the page.
146193 var getTokenVars = {
147194 'action':'query',
@@ -151,10 +198,10 @@
152199 'format':'json'
153200 };
154201
155 - $j.get( wgScriptPath + '/api.php', getTokenVars,
 202+ $.get( wgScriptPath + '/api.php', getTokenVars,
156203 function( reply ) {
157204 var infos = reply.query.pages;
158 - $j.each(
 205+ $.each(
159206 infos,
160207 function( pageid, data ) {
161208 var token = data.edittoken;
@@ -175,152 +222,294 @@
176223 'format':'json'
177224 };
178225
179 - $j.post( wgScriptPath + '/api.php', postEditVars, doneFn, 'json' );
 226+ $.post( wgScriptPath + '/api.php', postEditVars, doneFn, 'json' );
180227 }
181228 );
182229 }
183230 , 'json' );
184 - },
 231+ }
185232
186 - addProgressIndicator : function( elem ) {
187 - var indicator = $j( '<div/>' );
 233+ function _addProgressIndicator ( elem ) {
 234+ var indicator = $( '<div/>' );
188235
189236 indicator.addClass( 'mw-ajax-loader' );
190237
191238 elem.append( indicator );
192 - },
 239+ }
193240
194 - removeProgressIndicator : function( elem ) {
 241+ function _removeProgressIndicator ( elem ) {
195242 elem.find( '.mw-ajax-loader' ).remove();
196 - },
197 -
198 - handleCategoryAdd : function( e ) {
199 - // Grab category text
200 - var category = $j( '#mw-addcategory-input' ).val();
201 - var appendText = "\n[[" + wgFormattedNamespaces[14] + ":" + category + "]]\n";
202 - var summary = gM( 'ajax-add-category-summary', category );
203 -
204 - ajaxCategories.confirmEdit(
205 - wgPageName,
206 - function( oldText ) { return oldText + appendText },
207 - summary,
208 - ajaxCategories.reloadCategoryList
209 - );
210 - },
211 -
212 - handleDeleteLink : function( e ) {
213 - e.preventDefault();
214 -
215 - var category = $j( this ).parent().find( 'a' ).text();
216 -
 243+ }
 244+
 245+ function _makeCaseInsensitiv( string ) {
 246+ var newString = '';
 247+ for (var i=0; i < string.length; i++) {
 248+ newString += '[' + string[i].toUpperCase() + string[i].toLowerCase() + ']';
 249+ };
 250+ return newString;
 251+ }
 252+ function _buildRegex ( category ) {
217253 // Build a regex that matches legal invocations of that category.
218 -
219 - // In theory I should escape the aliases, but there's no JS function for it
220 - // Shouldn't have any real impact, can't be exploited or anything, so we'll
221 - // leave it for now.
222254 var categoryNSFragment = '';
223 - $j.each( wgNamespaceIds, function( name, id ) {
 255+ $.each( namespaceIds, function( name, id ) {
224256 if ( id == 14 ) {
225 - // Allow the first character to be any case
226 - var firstChar = name.charAt( 0 );
227 - firstChar = '[' + firstChar.toUpperCase() + firstChar.toLowerCase() + ']';
228 - categoryNSFragment += '|' + firstChar + name.substr( 1 );
 257+ // The parser accepts stuff like cATegORy,
 258+ // we need to do the same
 259+ categoryNSFragment += '|' + _makeCaseInsensitiv ( $.escapeRE(name) );
229260 }
230261 } );
231262 categoryNSFragment = categoryNSFragment.substr( 1 ); // Remove leading |
232 -
 263+
233264 // Build the regex
234 - var titleFragment = category;
 265+ var titleFragment = $.escapeRE(category);
235266
236267 firstChar = category.charAt( 0 );
237268 firstChar = '[' + firstChar.toUpperCase() + firstChar.toLowerCase() + ']';
238269 titleFragment = firstChar + category.substr( 1 );
239 - var categoryRegex = '\\[\\[' + categoryNSFragment + ':' + titleFragment + '(\\|[^\\]]*)?\\]\\]';
240 - categoryRegex = new RegExp( categoryRegex, 'g' );
 270+ var categoryRegex = '\\[\\[(' + categoryNSFragment + '):' + titleFragment + '(\\|[^\\]]*)?\\]\\]';
241271
242 - var summary = gM( 'ajax-remove-category-summary', category );
 272+ return new RegExp( categoryRegex, 'g' );
 273+ }
 274+
 275+ function _handleEditLink ( e ) {
 276+ e.preventDefault();
 277+ var $this = $( this );
 278+ var $link = $this.parent().find( 'a:not(.icon)' );
 279+ var category = $link.text();
 280+
 281+ var $input = _makeSuggestionBox( category, _handleCategoryEdit, mw.msg( 'ajax-confirm-save' ) );
 282+ $link.after( $input ).hide();
 283+ _catElements[category].editButton.hide();
 284+ _catElements[category].deleteButton.unbind('click').click( function() {
 285+ $input.remove();
 286+ $link.show();
 287+ _catElements[category].editButton.show();
 288+ $( this ).unbind('click').click( _handleDeleteLink );
 289+ });
 290+ }
 291+
 292+ function _handleAddLink ( e ) {
 293+ e.preventDefault();
243294
244 - ajaxCategories.confirmEdit(
245 - wgPageName,
 295+ $container.find( '#mw-normal-catlinks>.mw-addcategory-prompt' ).toggle();
 296+ }
 297+
 298+ function _handleDeleteLink ( e ) {
 299+ e.preventDefault();
 300+
 301+ var $this = $( this );
 302+ var $link = $this.parent().find( 'a:not(.icon)' );
 303+ var category = $link.text();
 304+
 305+ categoryRegex = _buildRegex( category );
 306+
 307+ var summary = mw.msg( 'ajax-remove-category-summary', category );
 308+
 309+ _confirmEdit(
 310+ mw.config.get('wgPageName'),
246311 function( oldText ) {
 312+ //TODO Cleanup whitespace safely?
247313 var newText = oldText.replace( categoryRegex, '' );
248314
249315 if ( newText == oldText ) {
250 - var error = gM( 'ajax-remove-category-error' );
251 - ajaxCategories.showError( error );
252 - ajaxCategories.removeProgressIndicator( $j( '.mw-ajax-confirm-dialog' ) );
253 - $j( '.mw-ajax-confirm-dialog' ).dialog( 'close' );
 316+ var error = mw.msg( 'ajax-remove-category-error' );
 317+ _showError( error );
 318+ _removeProgressIndicator( $( '.mw-ajax-confirm-dialog' ) );
 319+ $( '.mw-ajax-confirm-dialog' ).dialog( 'close' );
254320 return false;
255321 }
256322
257323 return newText;
258324 },
259 - summary, ajaxCategories.reloadCategoryList
 325+ summary,
 326+ function() {
 327+ $this.parent().remove();
 328+ }
260329 );
261 - },
 330+ }
262331
263 - showError : function( str ) {
264 - var dialog = $j( '<div/>' );
 332+ function _handleCategoryAdd ( e ) {
 333+ // Grab category text
 334+ var category = $( this ).parent().find( '.mw-addcategory-input' ).val();
 335+ category = $.ucFirst( category );
 336+
 337+ if ( _containsCat(category) ) {
 338+ // TODO add info alert
 339+ return;
 340+ }
 341+ var appendText = "\n[[" + categoryNamespace + ":" + category + "]]\n";
 342+ var summary = mw.msg( 'ajax-add-category-summary', category );
 343+
 344+ _confirmEdit(
 345+ mw.config.get( 'wgPageName' ),
 346+ function( oldText ) { return oldText + appendText },
 347+ summary,
 348+ function() {
 349+ _insertCatDOM( category, false );
 350+ }
 351+ );
 352+ }
 353+
 354+ function _handleCategoryEdit ( e ) {
 355+ e.preventDefault();
 356+
 357+ // Grab category text
 358+ var categoryNew = $( this ).parent().find( '.mw-addcategory-input' ).val();
 359+ categoryNew = $.ucFirst( categoryNew );
 360+
 361+ var $this = $( this );
 362+ var $link = $this.parent().parent().find( 'a:not(.icon)' );
 363+ var category = $link.text();
 364+
 365+ // User didn't change anything. Just close the box
 366+ if ( category == categoryNew ) {
 367+ $this.parent().remove();
 368+ $link.show();
 369+ return;
 370+ }
 371+ categoryRegex = _buildRegex( category );
 372+
 373+ var summary = mw.msg( 'ajax-edit-category-summary', category, categoryNew );
 374+
 375+ _confirmEdit(
 376+ mw.config.get( 'wgPageName' ),
 377+ function( oldText ) {
 378+ var matches = oldText.match( categoryRegex );
 379+
 380+ //Old cat wasn't found, likely to be transcluded
 381+ if ( !$.isArray( matches ) ) {
 382+ var error = mw.msg( 'ajax-edit-category-error' );
 383+ _showError( error );
 384+ _removeProgressIndicator( $( '.mw-ajax-confirm-dialog' ) );
 385+ $( '.mw-ajax-confirm-dialog' ).dialog( 'close' );
 386+ return false;
 387+ }
 388+ var sortkey = matches[0].replace( categoryRegex, '$2' );
 389+ var newCategoryString = "[[" + categoryNamespace + ":" + categoryNew + sortkey + ']]';
 390+
 391+ if (matches.length > 1) {
 392+ // The category is duplicated.
 393+ // Remove all but one match
 394+ for (var i = 1; i < matches.length; i++) {
 395+ oldText = oldText.replace( matches[i], '');
 396+ }
 397+ }
 398+ var newText = oldText.replace( categoryRegex, newCategoryString );
 399+
 400+ return newText;
 401+ },
 402+ summary,
 403+ function() {
 404+ // Remove input box & button
 405+ $this.parent().remove();
 406+
 407+ // Update link text and href
 408+ $link.show().text( categoryNew ).attr( 'href', _catLink( categoryNew ) );
 409+ }
 410+ );
 411+ }
 412+ function _showError ( str ) {
 413+ var dialog = $( '<div/>' );
265414 dialog.text( str );
266415
267 - $j( '#bodyContent' ).append( dialog );
 416+ $( '#bodyContent' ).append( dialog );
268417
269418 var buttons = { };
270 - buttons[gM( 'ajax-error-dismiss' )] = function( e ) {
 419+ buttons[mw.msg( 'ajax-error-dismiss' )] = function( e ) {
271420 dialog.dialog( 'close' );
272421 };
273422 var dialogOptions = {
274423 'buttons' : buttons,
275424 'AutoOpen' : true,
276 - 'title' : gM( 'ajax-error-title' )
 425+ 'title' : mw.msg( 'ajax-error-title' )
277426 };
278427
279428 dialog.dialog( dialogOptions );
280 - },
 429+ }
281430
282 - setupAJAXCategories : function() {
 431+ function _createButton ( icon, title, category, text ){
 432+ var button = $( '<a>' ).addClass( category || '' )
 433+ .attr('title', title);
 434+
 435+ if ( text ) {
 436+ var icon = $( '<a>' ).addClass( 'icon ' + icon );
 437+ button.addClass( 'icon-parent' ).append( icon ).append( text );
 438+ } else {
 439+ button.addClass( 'icon ' + icon );
 440+ }
 441+ return button;
 442+ }
 443+ function _createCatButtons ( element ) {
 444+ // Create remove & edit buttons
 445+ var deleteButton = _createButton('icon-close', mw.msg( 'ajax-remove-category' ) );
 446+ var editButton = _createButton('icon-edit', mw.msg( 'ajax-edit-category' ) );
 447+
 448+ //Not yet used
 449+ var saveButton = _createButton('icon-tick', mw.msg( 'ajax-confirm-save' ) ).hide();
 450+
 451+ deleteButton.click( _handleDeleteLink );
 452+ editButton.click( _handleEditLink );
 453+
 454+ $( element ).after( deleteButton ).after( editButton );
 455+
 456+ //Save references to all links and buttons
 457+ _catElements[$( element ).text()] = {
 458+ link : $( element ),
 459+ parent : $( element ).parent(),
 460+ saveButton : saveButton,
 461+ deleteButton: deleteButton,
 462+ editButton : editButton
 463+ };
 464+ }
 465+ function _setup() {
 466+ // Could be set by gadgets like HotCat etc.
 467+ if ( mw.config.get('disableAJAXCategories') ) {
 468+ return;
 469+ }
283470 // Only do it for articles.
284 - if ( !wgIsArticle ) return;
 471+ if ( !mw.config.get( 'wgIsArticle' ) ) return;
285472
286 - var clElement = $j( '.catlinks' );
 473+ var clElement = $( '#mw-normal-catlinks' );
287474
288475 // Unhide hidden category holders.
289 - clElement.removeClass( 'catlinks-allhidden' );
 476+ $('#mw-hidden-catlinks').show();
290477
291 - var addLink = $j( '<a/>' );
292 - addLink.addClass( 'mw-ajax-addcategory' );
293478
294479 // Create [Add Category] link
295 - addLink.text( gM( 'ajax-add-category' ) );
296 - addLink.attr( 'href', '#' );
297 - addLink.click( ajaxCategories.handleAddLink );
 480+ var addLink = _createButton('icon-add',
 481+ mw.msg( 'ajax-add-category' ),
 482+ 'mw-ajax-addcategory',
 483+ mw.msg( 'ajax-add-category' )
 484+ );
 485+ addLink.click( _handleAddLink );
298486 clElement.append( addLink );
299487
300488 // Create add category prompt
301 - var promptContainer = $j( '<div id="mw-addcategory-prompt"/>' );
302 - var promptTextbox = $j( '<input type="text" size="45" id="mw-addcategory-input"/>' );
303 - var addButton = $j( '<input type="button" id="mw-addcategory-button"/>' );
304 - addButton.val( gM( 'ajax-add-category-submit' ) );
305 -
306 - promptTextbox.keypress( ajaxCategories.handleCategoryInput );
307 - addButton.click( ajaxCategories.handleCategoryAdd );
308 -
309 - promptContainer.append( promptTextbox );
310 - promptContainer.append( addButton );
 489+ var promptContainer = _makeSuggestionBox( '', _handleCategoryAdd, mw.msg( 'ajax-add-category-submit' ) );
311490 promptContainer.hide();
312491
313 - // Create delete link for each category.
314 - $j( '.catlinks div span a' ).each( function( e ) {
315 - // Create a remove link
316 - var deleteLink = $j( '<a class="mw-remove-category" href="#"/>' );
 492+ // Create edit & delete link for each category.
 493+ $( '#catlinks li a' ).each( function( e ) {
 494+ _createCatButtons( this );
 495+ });
317496
318 - deleteLink.click( ajaxCategories.handleDeleteLink );
319 -
320 - $j( this ).after( deleteLink );
321 - } );
322 -
323497 clElement.append( promptContainer );
324498 }
325 -};
 499+ function _teardown() {
 500+
 501+ }
 502+ _tasks = {
 503+ list : [],
 504+ executed : [],
 505+ add : function( obj ) {
 506+ this.list.push( obj );
 507+ },
 508+ next : function() {
 509+ var task = this.list.shift();
 510+ //run task
 511+ this.executed.push( task );
 512+ }
 513+ }
 514+ $(document).ready( function() {_setup()});
326515
327 -js2AddOnloadHook( ajaxCategories.setupAJAXCategories );
 516+} )( jQuery, mediaWiki );
\ No newline at end of file
Index: trunk/phase3/resources/mediawiki.page/images/AJAXCategorySprite.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: trunk/phase3/resources/mediawiki.page/images/AJAXCategorySprite.png
___________________________________________________________________
Added: svn:mime-type
328517 + image/png
Index: trunk/phase3/resources/mediawiki.page/mediawiki.page.ajaxCategories.css
@@ -0,0 +1,62 @@
 2+.mw-addcategory-prompt {
 3+ display: inline;
 4+}
 5+
 6+.mw-addcategory-prompt input {
 7+ margin-left: 0.5em;
 8+ margin-right: 0.5em;
 9+}
 10+
 11+.mw-remove-category {
 12+ padding: 2px 8px;
 13+ display:inline;
 14+}
 15+
 16+#catlinks:hover .icon {
 17+ opacity: 1;
 18+}
 19+
 20+.mw-ajax-addcategory {
 21+ padding-left: 30px;
 22+ margin-right: 1em;
 23+ cursor: pointer;
 24+}
 25+#catlinks .icon {
 26+ cursor: pointer;
 27+ padding: 1px 8px;
 28+ margin: 0;
 29+ background: url('images/AJAXCategorySprite.png') 0 0 no-repeat;
 30+ opacity: 0.5;
 31+}
 32+#catlinks .icon-parent {
 33+ cursor: pointer;
 34+}
 35+#catlinks .icon-parent:hover .icon {
 36+ background-position-y: -16px;
 37+}
 38+#catlinks .no-text {
 39+}
 40+#catlinks .icon-close {
 41+ background-position: 0 0;
 42+}
 43+#catlinks .icon-edit {
 44+ background-position: -16px 0;
 45+}
 46+#catlinks .icon-tick {
 47+ background-position: -32px 0;
 48+}
 49+#catlinks .icon-add {
 50+ background-position: -64px 0;
 51+}
 52+#catlinks .icon-close:hover {
 53+ background-position: 0 -16px;
 54+}
 55+#catlinks .icon-edit:hover {
 56+ background-position: -16px -16px;
 57+}
 58+#catlinks .icon-tick:hover {
 59+ background-position: -32px -16px;
 60+}
 61+#catlinks .icon-add:hover {
 62+ background-position: -64px -16px;
 63+}
\ No newline at end of file
Property changes on: trunk/phase3/resources/mediawiki.page/mediawiki.page.ajaxCategories.css
___________________________________________________________________
Added: svn:eol-style
164 + native

Follow-up revisions

RevisionCommit summaryAuthorDate
r92151AjaxCategories:...diebuche11:36, 14 July 2011
r92159AjaxCategories:...diebuche15:00, 14 July 2011
r92164AjaxCategories: Yay! MultiEdit mode working. May need more polish. Ping r92112diebuche16:00, 14 July 2011
r92288ajaxCategories fixes:...krinkle19:25, 15 July 2011
r93090* Ignore any <nowiki> or comment sections...diebuche19:03, 25 July 2011

Comments

#Comment by Nikerabbit (talk | contribs)   23:22, 13 July 2011

Rewrite? Where is the old code? Why isn't this enabled by default?

#Comment by DieBuche (talk | contribs)   00:08, 14 July 2011

The old code slumbered in /branches/js2 for 1 1/2 years. Enabled per default: Not yet, there are a few kinks left.

#Comment by 😂 (talk | contribs)   00:14, 14 July 2011

I remember this, I just didn't remember it being off in JS2-land. Glad to see it land in core. Let's definitely iron out the kinks and enable this by default before next release.

#Comment by Krinkle (talk | contribs)   00:25, 14 July 2011

Nice work on bringing that back!

I know it's just a start, but a few things:

+	$(document).ready( function() {_setup()});

No need to create an anonymous function to call a function, pass the function by reference directly.

+	var _catElements = {};
+	var _otherElements = {};

This closure is local (private) entirely, none of them are public, why are only these underscored / why underscored at all ?


-var ajaxCategories = {
-	fetchSuggestions : function( query ) {
+	function _fetchSuggestions ( query ) {

same thing, going from a public method to a local one is a choise, but why underscore it ? If its public but supposed to be private, it could make sense to underscore it if one is unable to make it private, but these really are private.

wgScriptPath + '/api.php'
Legacy global + hardcoded ".php", use mw.util.wikiScript( 'api' ) instead.
#Comment by Krinkle (talk | contribs)   00:29, 14 July 2011

Hm.. seems the entire thing is now in seperate local functions, no hooks, not testable and self-executing.

Please make this module not self-executing (execute in mediawiki.page.ready) and testable. Both require it to be accessible from the mediaWiki object. mw.ajaxCategories would make sense, it was an object oriented script.

#Comment by DieBuche (talk | contribs)   01:07, 14 July 2011
#Comment by Krinkle (talk | contribs)   20:44, 15 July 2011

Why did you change it from an object oriented script into separate variables and functions ? The original wasn't perfect and wasn't a a proper object constructor with prototype functions and an options object either, but going away from that into even less "object orientation" seems wrong to me.

#Comment by Schnark (talk | contribs)   08:50, 19 July 2011

Just a few things I saw when reading the code (I don't want to open a bug for each of these things, if you want to have bugs for it, you must open them yourself):

  • _doEdit should check whether the edit was successful or whether the API returned an error.
  • containsCat and _buildRegex assume that the first letter in category namespace is case-insensitive. This needn't always be the case, use wgCaseSensitiveNamespaces to test this.
  • _makeCaseInsensitive and _buildRegex could produce a regular expression with [--] in it if there is an alias for Category: with a hyphen in it resp. the first character of the category is a hyphen. A similar problem occurs with ^.
  • combinedFn in _handleStashedCategories should abort early if one of the functions returns false.
  • Categories are changed even in nowiki tags, HTML comments etc. Things like {{Foo}} <!-- Note: [[Category:Foo]] is added by this template! --> probably occur in reality.
  • The regular expressions for changing/removing categories aren't really fool-proof: At least you must take spaces after Category: into consideration and the equivalence of space and underscore. Other things to consider are %-encoding and HTML-entities: Category:Ä could be represented as [[Category:_%C2%A0&auml;]] in the source. If you really program something that takes care of all that nonsense then put it somewhere where it can be reused in gadgets/user scripts, at least I would want to reuse it in one of my user scripts.
  • Adding a category just puts it at the end of the text. If there already are categories it should be put after the last one. For other wishes (like: put the category before the first interwiki link) hooks that run after the change would be helpful.
  • Hooked functions should be allowed to return false to abort editing.
  • The category to be added/changed/removed should be passed to the hook functions in some way.
#Comment by DieBuche (talk | contribs)   10:06, 19 July 2011

Thanks for the comments. Two things I don't agree with:

  • 3: [--] is not exactly beautiful, but it doesn't matter since it only matches "-" ( and not maybe "--" )
  • 6: I'm not gonna tackle those weird entities. It'll throw an error and those should be rare enough for it not to happen to often. If we wan't a 100% foolproof system, we should move away from storing stuff like this inside the wikitext.

I'll implement the other ones

#Comment by DieBuche (talk | contribs)   19:06, 25 July 2011

Done in r93090

#Comment by Krinkle (talk | contribs)   07:44, 30 July 2011

Addressed points related to the original script and the changes made here and in the follow-ups in r93351. Marking OK.

Status & tagging log