r96250 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r96249‎ | r96250 | r96251 >
Date:19:35, 4 September 2011
Author:krinkle
Status:ok
Tags:
Comment:
Split ajaxCategories away from core for now, into an extension:

* Move js/css/images from core into extension dir (kept svn history in tact)

* Removed from core:
** Messages
** Resource definition
** Default settings

* Recreated in extension:
** Messages with 'inlinecategorizer' prefix (instead of generic 'ajax' prefix).
** wgResourceModule definition
** Hooks for adding module to the page
** Setting wgAJAXCategoriesNamespaces kept (renamed to $wgInlineCategorizerNamespaces)

* Made minor adjustments to the messages:
** Fixed references to other messages (in /qqq) with the new message key

* Made minor adjustments to the javascript:
** Usage of mw.msg fixed to the new message keys
** Removed "@since 1.19"
** Removed "Relies on mw.user.getId" (because it didn't', and still doesn't)
** Object 'mw.ajaxCategories' -> 'mw.InlineCategorizer' (capitalized, since it's a constructor, per our conventions)
** Removed 'disableAJAXCategories' config, not needed.


Summary of justification for move out of core (again):
* Too many issues with the parsing logic (many cases still don't work yet)
* Almost untested and untestable because of mixing UI with logic code. Needs separation.
* See http://www.mediawiki.org/wiki/User:Krinkle/Extension_review/InlineCategorizer#Prequel

One day we might move it in, or if the "new parser" is ready and the "visual editor" we might not need it all together and it'd simply be a 10-20 line module in the visual editor (that, or it'd be part of the visual editor by default).

Anyway, I'm not against this module. if we can get this to an acceptable state soonish before any of the new parser / visual editor is ready, then I see no problem in bundling it with core and/or merging it into core.
Modified paths:
  • /trunk/extensions/InlineCategorizer/InlineCategorizer.hooks.php (added) (history)
  • /trunk/extensions/InlineCategorizer/InlineCategorizer.i18n.php (added) (history)
  • /trunk/extensions/InlineCategorizer/InlineCategorizer.php (added) (history)
  • /trunk/extensions/InlineCategorizer/modules (added) (history)
  • /trunk/extensions/InlineCategorizer/modules/ext.InlineCategorizer.core.css (added) (history)
  • /trunk/extensions/InlineCategorizer/modules/ext.InlineCategorizer.core.js (added) (history)
  • /trunk/extensions/InlineCategorizer/modules/ext.InlineCategorizer.init.js (added) (history)
  • /trunk/extensions/InlineCategorizer/modules/ext.inlineCategorizer.core.css (added) (history)
  • /trunk/extensions/InlineCategorizer/modules/ext.inlineCategorizer.core.js (added) (history)
  • /trunk/extensions/InlineCategorizer/modules/ext.inlineCategorizer.init.js (added) (history)
  • /trunk/extensions/InlineCategorizer/modules/images (added) (history)
  • /trunk/phase3/includes/DefaultSettings.php (modified) (history)
  • /trunk/phase3/includes/OutputPage.php (modified) (history)
  • /trunk/phase3/languages/messages/MessagesEn.php (modified) (history)
  • /trunk/phase3/languages/messages/MessagesQqq.php (modified) (history)
  • /trunk/phase3/maintenance/language/messages.inc (modified) (history)
  • /trunk/phase3/resources/Resources.php (modified) (history)
  • /trunk/phase3/resources/mediawiki.page/images (deleted) (history)
  • /trunk/phase3/resources/mediawiki.page/mediawiki.page.ajaxCategories.css (deleted) (history)
  • /trunk/phase3/resources/mediawiki.page/mediawiki.page.ajaxCategories.init.js (deleted) (history)
  • /trunk/phase3/resources/mediawiki.page/mediawiki.page.ajaxCategories.js (deleted) (history)

Diff [purge]

Index: trunk/phase3/maintenance/language/messages.inc
@@ -3504,30 +3504,6 @@
35053505 'unwatch' => array(
35063506 'confirm-unwatch-button',
35073507 ),
3508 - 'ajax-category' => array(
3509 - 'ajax-add-category',
3510 - 'ajax-remove-category',
3511 - 'ajax-edit-category',
3512 - 'ajax-add-category-submit',
3513 - 'ajax-confirm-ok',
3514 - 'ajax-confirm-title',
3515 - 'ajax-confirm-save',
3516 - 'ajax-confirm-save-all',
3517 - 'ajax-cancel',
3518 - 'ajax-cancel-all',
3519 - 'ajax-add-category-summary',
3520 - 'ajax-edit-category-summary',
3521 - 'ajax-remove-category-summary',
3522 - 'ajax-category-question',
3523 - 'ajax-error-title',
3524 - 'ajax-remove-category-error',
3525 - 'ajax-edit-category-error',
3526 - 'ajax-category-already-present',
3527 - 'ajax-category-hook-error',
3528 - 'ajax-api-error',
3529 - 'ajax-api-unknown-error',
3530 - ),
3531 -
35323508 );
35333509
35343510 /** Comments for each block */
@@ -3759,5 +3735,4 @@
37603736 'db-error-messages' => 'Database error messages',
37613737 'html-forms' => 'HTML forms',
37623738 'sqlite' => 'SQLite database support',
3763 - 'ajax-category' => 'Add categories per AJAX',
37643739 );
Index: trunk/phase3/includes/OutputPage.php
@@ -2296,8 +2296,7 @@
22972297 * Add the default ResourceLoader modules to this object
22982298 */
22992299 private function addDefaultModules() {
2300 - global $wgIncludeLegacyJavaScript, $wgUseAjax,
2301 - $wgAjaxWatch, $wgEnableMWSuggest, $wgUseAJAXCategories;
 2300+ global $wgIncludeLegacyJavaScript, $wgUseAjax, $wgAjaxWatch, $wgEnableMWSuggest;
23022301
23032302 // Add base resources
23042303 $this->addModules( array(
@@ -2333,16 +2332,6 @@
23342333 if ( $this->isArticle() && $this->getUser()->getOption( 'editondblclick' ) ) {
23352334 $this->addModules( 'mediawiki.action.view.dblClickEdit' );
23362335 }
2337 -
2338 - if ( $wgUseAJAXCategories ) {
2339 - global $wgAJAXCategoriesNamespaces;
2340 -
2341 - $title = $this->getTitle();
2342 -
2343 - if( empty( $wgAJAXCategoriesNamespaces ) || in_array( $title->getNamespace(), $wgAJAXCategoriesNamespaces ) ) {
2344 - $this->addModules( 'mediawiki.page.ajaxCategories.init' );
2345 - }
2346 - }
23472336 }
23482337
23492338 /**
Index: trunk/phase3/includes/DefaultSettings.php
@@ -5620,19 +5620,6 @@
56215621 $wgDBtestpassword = '';
56225622
56235623 /**
5624 - * Whether or not to use the AJAX categories system.
5625 - */
5626 -$wgUseAJAXCategories = false;
5627 -
5628 -/**
5629 - * Only enable AJAXCategories on configured namespaces. Default is all.
5630 - *
5631 - * Example:
5632 - * $wgAJAXCategoriesNamespaces = array( NS_MAIN, NS_PROJECT );
5633 - */
5634 -$wgAJAXCategoriesNamespaces = array();
5635 -
5636 -/**
56375624 * For really cool vim folding this needs to be at the end:
56385625 * vim: foldmarker=@{,@} foldmethod=marker
56395626 * @}
Index: trunk/phase3/languages/messages/MessagesQqq.php
@@ -4300,23 +4300,4 @@
43014301 'sqlite-has-fts' => 'Shown on Special:Version, $1 is version',
43024302 'sqlite-no-fts' => 'Shown on Special:Version, $1 is version',
43034303
4304 -# Add categories per AJAX
4305 -'ajax-remove-category' => 'Tooltip for link to remove a category from the page, displayed after each category at the foot of a page.
4306 -Refers to the specific category. "Remove this category" is also correct.',
4307 -'ajax-edit-category' => 'Tooltip for the edit link displayed after each category at the foot of a page. Refers to the specific category. "Edit this category" is also correct.',
4308 -'ajax-add-category-submit' => '{{Identical|Add}}',
4309 -'ajax-confirm-ok' => '{{Identical|OK}}',
4310 -'ajax-confirm-title' => 'Title for a dialog box in which the user is asked for an edit summary',
4311 -'ajax-confirm-save' => 'Submit button {{Identical|Save}}',
4312 -'ajax-confirm-save-all' => 'Submit button to save all changes',
4313 -'ajax-add-category-summary' => 'See {{msg-mw|ajax-category-question}}. $1 is a category name.',
4314 -'ajax-edit-category-summary' => 'See {{msg-mw|ajax-category-question}}. $1 and $2 are both category names.',
4315 -'ajax-remove-category-summary' => 'See {{msg-mw|ajax-category-question}}. $1 is a category name.',
4316 -'ajax-category-question' => "Question the user is asked before submit. It's followed by a list of the changes.",
4317 -'ajax-error-title' => '{{Identical|Error}}',
4318 -'ajax-category-already-present' => 'Error message. $1 is the category name',
4319 -'ajax-api-error' => 'API = [http://en.wikipedia.org/wiki/Application_programming_interface Application programming interface].
4320 -
4321 -"returned" here means "reported".',
4322 -
43234304 );
Index: trunk/phase3/languages/messages/MessagesEn.php
@@ -4636,29 +4636,4 @@
46374637 'sqlite-has-fts' => '$1 with full-text search support',
46384638 'sqlite-no-fts' => '$1 without full-text search support',
46394639
4640 -# Add categories per AJAX
4641 -'ajax-add-category' => 'Add category',
4642 -'ajax-remove-category' => 'Remove category',
4643 -'ajax-edit-category' => 'Edit category',
4644 -'ajax-add-category-submit' => 'Add',
4645 -'ajax-confirm-ok' => 'OK',
4646 -'ajax-confirm-title' => 'Confirm action',
4647 -'ajax-confirm-save' => 'Save',
4648 -'ajax-confirm-save-all' => 'Save all changes',
4649 -'ajax-cancel' => 'Cancel edit',
4650 -'ajax-cancel-all' => 'Cancel all changes',
4651 -'ajax-add-category-summary' => 'Add category "$1"',
4652 -'ajax-edit-category-summary' => 'Change category "$1" to "$2"',
4653 -'ajax-remove-category-summary' => 'Remove category "$1"',
4654 -'ajax-category-question' => 'Why do you want to make the following changes:',
4655 -'ajax-error-title' => 'Error',
4656 -'ajax-remove-category-error' => 'It was not possible to remove category "$1".
4657 -This usually occurs when the category has been added to the page in a template.',
4658 -'ajax-edit-category-error' => 'It was not possible to edit category "$1".
4659 -This usually occurs when the category has been added to the page in a template.',
4660 -'ajax-category-already-present' => 'This page already belongs to the category "$1"',
4661 -'ajax-category-hook-error' => 'A local function prevented the changes from being saved.',
4662 -'ajax-api-error' => 'The API returned an error: $1: $2.',
4663 -'ajax-api-unknown-error' => 'The API returned an unknown error.',
4664 -
46654640 );
Index: trunk/phase3/resources/Resources.php
@@ -602,45 +602,6 @@
603603 'jquery.mwExtension',
604604 ),
605605 ),
606 - 'mediawiki.page.ajaxCategories' => array(
607 - 'scripts' => 'resources/mediawiki.page/mediawiki.page.ajaxCategories.js',
608 - 'styles' => 'resources/mediawiki.page/mediawiki.page.ajaxCategories.css',
609 - 'dependencies' => array(
610 - 'jquery.suggestions',
611 - 'jquery.ui.dialog',
612 - 'mediawiki.Title',
613 - ),
614 - 'messages' => array(
615 - 'ajax-add-category',
616 - 'ajax-remove-category',
617 - 'ajax-edit-category',
618 - 'ajax-add-category-submit',
619 - 'ajax-confirm-ok',
620 - 'ajax-confirm-title',
621 - 'ajax-confirm-save',
622 - 'ajax-confirm-save-all',
623 - 'ajax-cancel',
624 - 'ajax-cancel-all',
625 - 'ajax-add-category-summary',
626 - 'ajax-edit-category-summary',
627 - 'ajax-remove-category-summary',
628 - 'ajax-category-question',
629 - 'ajax-category-and',
630 - 'ajax-error-title',
631 - 'ajax-remove-category-error',
632 - 'ajax-edit-category-error',
633 - 'ajax-category-already-present',
634 - 'ajax-category-hook-error',
635 - 'ajax-api-error',
636 - 'ajax-api-unknown-error',
637 - ),
638 - ),
639 - 'mediawiki.page.ajaxCategories.init' => array(
640 - 'scripts' => 'resources/mediawiki.page/mediawiki.page.ajaxCategories.init.js',
641 - 'dependencies' => array(
642 - 'mediawiki.page.ajaxCategories',
643 - ),
644 - ),
645606 'mediawiki.page.ready' => array(
646607 'scripts' => 'resources/mediawiki.page/mediawiki.page.ready.js',
647608 'dependencies' => array(
Index: trunk/phase3/resources/mediawiki.page/mediawiki.page.ajaxCategories.js
@@ -1,1155 +0,0 @@
2 -/**
3 - * mediaWiki.page.ajaxCategories
4 - *
5 - * @author Michael Dale, 2009
6 - * @author Leo Koppelkamm, 2011
7 - * @author Timo Tijhof, 2011
8 - * @since 1.19
9 - *
10 - * Relies on: mw.config (wgFormattedNamespaces, wgNamespaceIds,
11 - * wgCaseSensitiveNamespaces, wgUserGroups), mw.util.wikiGetlink, mw.user.getId
12 - */
13 -( function( $ ) {
14 -
15 - /* Local scope */
16 -
17 - var catNsId = mw.config.get( 'wgNamespaceIds' ).category,
18 - defaultOptions = {
19 - catLinkWrapper: '<li>',
20 - $container: $( '.catlinks' ),
21 - $containerNormal: $( '#mw-normal-catlinks' ),
22 - categoryLinkSelector: 'li a:not(.icon)',
23 - multiEdit: $.inArray( 'user', mw.config.get( 'wgUserGroups' ) ) !== -1,
24 - resolveRedirects: true
25 - },
26 - isCatNsSensitive = $.inArray( 14, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1;
27 -
28 - /**
29 - * @return {String}
30 - */
31 - function clean( s ) {
32 - if ( typeof s === 'string' ) {
33 - return s.replace( /[\x00-\x1f\x23\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f\s]+/g, '' );
34 - }
35 - return '';
36 - }
37 -
38 - /**
39 - * Generates a random id out of 62 alpha-numeric characters.
40 - *
41 - * @param {Number} Length of id (optional, defaults to 32)
42 - * @return {String}
43 - */
44 - function generateRandomId( idLength ) {
45 - idLength = typeof idLength === 'number' ? idLength : 32;
46 - var seed = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
47 - id = '';
48 - for ( var r, i = 0; i < idLength; i++ ) {
49 - r = Math.floor( Math.random() * seed.length );
50 - id += seed.substring( r, r + 1 );
51 - }
52 - return id;
53 - }
54 -
55 - /**
56 - * Helper function for $.fn.suggestions
57 - *
58 - * @context {jQuery}
59 - * @param value {String} Textbox value.
60 - */
61 - function fetchSuggestions( value ) {
62 - var request,
63 - $el = this,
64 - catName = clean( value );
65 -
66 - request = $.ajax( {
67 - url: mw.util.wikiScript( 'api' ),
68 - data: {
69 - action: 'query',
70 - list: 'allpages',
71 - apnamespace: catNsId,
72 - apprefix: catName,
73 - format: 'json'
74 - },
75 - dataType: 'json',
76 - success: function( data ) {
77 - // Process data.query.allpages into an array of titles
78 - var pages = data.query.allpages,
79 - titleArr = $.map( pages, function( page ) {
80 - return new mw.Title( page.title ).getMainText();
81 - } );
82 -
83 - $el.suggestions( 'suggestions', titleArr );
84 - }
85 - } );
86 - $el.data( 'suggestions-request', request );
87 - }
88 -
89 - /**
90 - * Replace <nowiki> and comments with unique keys in the page text.
91 - *
92 - * @param text {String}
93 - * @param id {String} Unique key for this nowiki replacement layer call.
94 - * @param keys {Array} Array where fragments will be stored in.
95 - * @return {String}
96 - */
97 - function replaceNowikis( text, id, keys ) {
98 - var matches = text.match( /(<nowiki\>[\s\S]*?<\/nowiki>|<\!--[\s\S]*?--\>)/g );
99 - for ( var i = 0; matches && i < matches.length; i++ ) {
100 - keys[i] = matches[i];
101 - text = text.replace( matches[i], '' + id + '-' + i );
102 - }
103 - return text;
104 - }
105 -
106 - /**
107 - * Restore <nowiki> and comments from unique keys in the page text.
108 - *
109 - * @param text {String}
110 - * @param id {String} Unique key of the layer to be restored, as passed to replaceNowikis().
111 - * @param keys {Array} Array where fragements should be fetched from.
112 - * @return {String}
113 - */
114 - function restoreNowikis( text, id, keys ) {
115 - for ( var i = 0; i < keys.length; i++ ) {
116 - text = text.replace( '' + id + '-' + i, keys[i] );
117 - }
118 - return text;
119 - }
120 -
121 - /**
122 - * Makes regex string caseinsensitive.
123 - * Useful when 'i' flag can't be used.
124 - * Return stuff like [Ff][Oo][Oo]
125 - *
126 - * @param string {String} Regex string
127 - * @return {String} Processed regex string
128 - */
129 - function makeCaseInsensitive( string ) {
130 - var newString = '';
131 - for ( var i = 0; i < string.length; i++ ) {
132 - newString += '[' + string.charAt( i ).toUpperCase() + string.charAt( i ).toLowerCase() + ']';
133 - }
134 - return newString;
135 - }
136 -
137 - /**
138 - * Build a regex that matches legal invocations of the passed category.
139 - * @param category {String}
140 - * @param matchLineBreak {Boolean} Match one following linebreak as well?
141 - * @return {RegExp}
142 - */
143 - function buildRegex( category, matchLineBreak ) {
144 - var categoryRegex, categoryNSFragment,
145 - titleFragment = $.escapeRE( category ).replace( /( |_)/g, '[ _]' ),
146 - firstChar = titleFragment.charAt( 0 );
147 -
148 - // Filter out all names for category namespace
149 - categoryNSFragment = $.map( mw.config.get( 'wgNamespaceIds' ), function( id, name ) {
150 - if ( id === catNsId ) {
151 - name = $.escapeRE( name );
152 - return !isCatNsSensitive ? makeCaseInsensitive( name ) : name;
153 - }
154 - // Otherwise don't include in categoryNSFragment
155 - return null;
156 - } ).join( '|' );
157 -
158 - firstChar = '[' + firstChar.toUpperCase() + firstChar.toLowerCase() + ']';
159 - titleFragment = firstChar + titleFragment.substr( 1 );
160 - categoryRegex = '\\[\\[(' + categoryNSFragment + ')' + '[ _]*' + ':' + '[ _]*' + titleFragment + '[ _]*' + '(\\|[^\\]]*)?\\]\\]';
161 - if ( matchLineBreak ) {
162 - categoryRegex += '[ \\t\\r]*\\n?';
163 - }
164 - return new RegExp( categoryRegex, 'g' );
165 - }
166 -
167 - /**
168 - * Manufacture iconed button, with or without text.
169 - *
170 - * @param icon {String} The icon class.
171 - * @param title {String} Title attribute.
172 - * @param className {String} (optional) Additional classes to be added to the button.
173 - * @param text {String} (optional) Text label of button.
174 - * @return {jQuery} The button.
175 - */
176 - function createButton( icon, title, className, text ){
177 - // We're adding a zero width space for IE7, it's got problems with empty nodes apparently
178 - var $button = $( '<a>' )
179 - .addClass( className || '' )
180 - .attr( 'title', title )
181 - .html( '&#8203;' );
182 -
183 - if ( text ) {
184 - var $icon = $( '<span>' ).addClass( 'icon ' + icon ).html( '&#8203;' );
185 - $button.addClass( 'icon-parent' ).append( $icon ).append( mw.html.escape( text ) );
186 - } else {
187 - $button.addClass( 'icon ' + icon );
188 - }
189 - return $button;
190 - }
191 -
192 -/**
193 - * @constructor
194 - * @param
195 - */
196 -mw.ajaxCategories = function( options ) {
197 -
198 - this.options = options = $.extend( defaultOptions, options );
199 -
200 - // Save scope in shortcut
201 - var ajaxcat = this;
202 -
203 - // Elements tied to this instance
204 - this.saveAllButton = null;
205 - this.cancelAllButton = null;
206 - this.addContainer = null;
207 -
208 - this.request = null;
209 -
210 - // Stash and hooks
211 - this.stash = {
212 - dialogDescriptions: [],
213 - editSummaries: [],
214 - fns: []
215 - };
216 - this.hooks = {
217 - beforeAdd: [],
218 - beforeChange: [],
219 - beforeDelete: [],
220 - afterAdd: [],
221 - afterChange: [],
222 - afterDelete: []
223 - };
224 -
225 - /* Event handlers */
226 -
227 - /**
228 - * Handle add category submit. Not to be called directly.
229 - *
230 - * @context Element
231 - * @param e {jQuery Event}
232 - */
233 - this.handleAddLink = function( e ) {
234 - var $el = $( this ),
235 - $link = $([]),
236 - categoryText = $.ucFirst( $el.parent().find( '.mw-addcategory-input' ).val() || '' );
237 -
238 - // Resolve redirects
239 - ajaxcat.resolveRedirects( categoryText, function( resolvedCatTitle ) {
240 - ajaxcat.handleCategoryAdd( $link, resolvedCatTitle, '', false );
241 - } );
242 - };
243 -
244 - /**
245 - * @context Element
246 - * @param e {jQuery Event}
247 - */
248 - this.createEditInterface = function( e ) {
249 - var $el = $( this ),
250 - $link = $el.data( 'link' ),
251 - category = $link.text(),
252 - $input = ajaxcat.makeSuggestionBox( category,
253 - ajaxcat.handleEditLink,
254 - ajaxcat.options.multiEdit ? mw.msg( 'ajax-confirm-ok' ) : mw.msg( 'ajax-confirm-save' )
255 - );
256 -
257 - $link.after( $input ).hide();
258 -
259 - $input.find( '.mw-addcategory-input' ).focus();
260 -
261 - // Get the editButton associated with this category link,
262 - // and hide it.
263 - $link.data( 'editButton' ).hide();
264 -
265 - // Get the deleteButton associated with this category link,
266 - $link.data( 'deleteButton' )
267 - // (re)set click handler
268 - .unbind( 'click' )
269 - .click( function() {
270 - // When the delete button is clicked:
271 - // - Remove the suggestion box
272 - // - Show the link and it's edit button
273 - // - (re)set the click handler again
274 - $input.remove();
275 - $link.show().data( 'editButton' ).show();
276 - $( this )
277 - .unbind( 'click' )
278 - .click( ajaxcat.handleDeleteLink )
279 - .attr( 'title', mw.msg( 'ajax-remove-category' ) );
280 - })
281 - .attr( 'title', mw.msg( 'ajax-cancel' ) );
282 - };
283 -
284 - /**
285 - * Handle edit category submit. Not to be called directly.
286 - *
287 - * @context Element
288 - * @param e {jQuery Event}
289 - */
290 - this.handleEditLink = function( e ) {
291 - var input, category, sortkey, categoryOld,
292 - $el = $( this ),
293 - $link = $el.parent().parent().find( 'a:not(.icon)' );
294 -
295 - // Grab category text
296 - input = $el.parent().find( '.mw-addcategory-input' ).val();
297 -
298 - // Split categoryname and sortkey
299 - var arr = input.split( '|', 2 );
300 - category = arr[0];
301 - sortkey = arr[1]; // Is usually undefined, ie. if there was no '|' in the input.
302 -
303 - // Grab text
304 - var isAdded = $link.hasClass( 'mw-added-category' );
305 - ajaxcat.resetCatLink( $link );
306 - categoryOld = $link.text();
307 -
308 - // If something changed and the new cat is already on the page, delete it.
309 - if ( categoryOld !== category && ajaxcat.containsCat( category ) ) {
310 - $link.data( 'deleteButton' ).click();
311 - return;
312 - }
313 -
314 - // Resolve redirects
315 - ajaxcat.resolveRedirects( category, function( resolvedCatTitle ) {
316 - ajaxcat.handleCategoryEdit( $link, categoryOld, resolvedCatTitle, sortkey, isAdded );
317 - });
318 - };
319 -
320 - /**
321 - * Handle delete category submit. Not to be called directly.
322 - *
323 - * @context Element
324 - * @param e {jQuery Event}
325 - */
326 - this.handleDeleteLink = function( e ) {
327 - var $el = $( this ),
328 - $link = $el.parent().find( 'a:not(.icon)' ),
329 - category = $link.text();
330 -
331 - if ( $link.is( '.mw-added-category, .mw-changed-category' ) ) {
332 - // We're just cancelling the addition or edit
333 - ajaxcat.resetCatLink( $link, $link.hasClass( 'mw-added-category' ) );
334 - return;
335 - } else if ( $link.is( '.mw-removed-category' ) ) {
336 - // It's already removed...
337 - return;
338 - }
339 - ajaxcat.handleCategoryDelete( $link, category );
340 - };
341 -
342 - /**
343 - * When multiEdit mode is enabled,
344 - * this is called when the user clicks "save all"
345 - * Combines the dialogDescriptions and edit functions.
346 - *
347 - * @context Element
348 - * @return ?
349 - */
350 - this.handleStashedCategories = function() {
351 -
352 - // Remove "holes" in array
353 - var dialogDescriptions = $.grep( ajaxcat.stash.dialogDescriptions, function( n, i ) {
354 - return n;
355 - } );
356 -
357 - if ( dialogDescriptions.length < 1 ) {
358 - // Nothing to do here.
359 - ajaxcat.saveAllButton.hide();
360 - ajaxcat.cancelAllButton.hide();
361 - return;
362 - } else {
363 - dialogDescriptions = dialogDescriptions.join( '<br/>' );
364 - }
365 -
366 - // Remove "holes" in array
367 - var summaryShort = $.grep( ajaxcat.stash.editSummaries, function( n,i ) {
368 - return n;
369 - } );
370 - summaryShort = summaryShort.join( ', ' );
371 -
372 - var fns = ajaxcat.stash.fns;
373 -
374 - ajaxcat.doConfirmEdit( {
375 - modFn: function( oldtext ) {
376 - // Run the text through all action functions
377 - var newtext = oldtext;
378 - for ( var i = 0; i < fns.length; i++ ) {
379 - if ( $.isFunction( fns[i] ) ) {
380 - newtext = fns[i]( newtext );
381 - if ( newtext === false ) {
382 - return false;
383 - }
384 - }
385 - }
386 - return newtext;
387 - },
388 - dialogDescription: dialogDescriptions,
389 - editSummary: summaryShort,
390 - doneFn: function() {
391 - ajaxcat.resetAll( true );
392 - },
393 - $link: null,
394 - action: 'all'
395 - } );
396 - };
397 -};
398 -
399 -/* Public methods */
400 -
401 -mw.ajaxCategories.prototype = {
402 - /**
403 - * Create the UI
404 - */
405 - setup: function() {
406 - // Could be set by gadgets like HotCat etc.
407 - if ( mw.config.get( 'disableAJAXCategories' ) ) {
408 - return false;
409 - }
410 - // Only do it for articles.
411 - if ( !mw.config.get( 'wgIsArticle' ) ) {
412 - return;
413 - }
414 - var options = this.options,
415 - ajaxcat = this,
416 - // Create [Add Category] link
417 - $addLink = createButton( 'icon-add',
418 - mw.msg( 'ajax-add-category' ),
419 - 'mw-ajax-addcategory',
420 - mw.msg( 'ajax-add-category' )
421 - ).click( function() {
422 - $( this ).nextAll().toggle().filter( '.mw-addcategory-input' ).focus();
423 - });
424 -
425 - // Create add category prompt
426 - this.addContainer = this.makeSuggestionBox( '', this.handleAddLink, mw.msg( 'ajax-add-category-submit' ) );
427 - this.addContainer.children().hide();
428 - this.addContainer.prepend( $addLink );
429 -
430 - // Create edit & delete link for each category.
431 - $( '#catlinks' ).find( 'li a' ).each( function() {
432 - ajaxcat.createCatButtons( $( this ) );
433 - });
434 -
435 - options.$containerNormal.append( this.addContainer );
436 -
437 - // @todo Make more clickable
438 - this.saveAllButton = createButton( 'icon-tick',
439 - mw.msg( 'ajax-confirm-save-all' ),
440 - '',
441 - mw.msg( 'ajax-confirm-save-all' )
442 - );
443 - this.cancelAllButton = createButton( 'icon-close',
444 - mw.msg( 'ajax-cancel-all' ),
445 - '',
446 - mw.msg( 'ajax-cancel-all' )
447 - );
448 - this.saveAllButton.click( this.handleStashedCategories ).hide();
449 - this.cancelAllButton.click( function() {
450 - ajaxcat.resetAll( false );
451 - } ).hide();
452 - options.$containerNormal.append( this.saveAllButton ).append( this.cancelAllButton );
453 - options.$container.append( this.addContainer );
454 - },
455 -
456 - /**
457 - * Insert a newly added category into the DOM.
458 - *
459 - * @param catTitle {mw.Title} Category title for which a link should be created.
460 - * @return {jQuery}
461 - */
462 - createCatLink: function( catTitle ) {
463 - var catName = catTitle.getMainText(),
464 - $catLinkWrapper = $( this.options.catLinkWrapper ),
465 - $anchor = $( '<a>' )
466 - .text( catName )
467 - .attr( {
468 - target: '_blank',
469 - href: catTitle.getUrl()
470 - } );
471 -
472 - $catLinkWrapper.append( $anchor );
473 -
474 - this.createCatButtons( $anchor );
475 -
476 - return $anchor;
477 - },
478 -
479 - /**
480 - * Create a suggestion box for use in edit/add dialogs
481 - * @param prefill {String} Prefill input
482 - * @param callback {Function} Called on submit
483 - * @param buttonVal {String} Button text
484 - */
485 - makeSuggestionBox: function( prefill, callback, buttonVal ) {
486 - // Create add category prompt
487 - var $promptContainer = $( '<div class="mw-addcategory-prompt"></div>' ),
488 - $promptTextbox = $( '<input type="text" size="30" class="mw-addcategory-input"></input>' ),
489 - $addButton = $( '<input type="button" class="mw-addcategory-button"></input>' ),
490 - ajaxcat = this;
491 -
492 - if ( prefill !== '' ) {
493 - $promptTextbox.val( prefill );
494 - }
495 -
496 - $addButton
497 - .val( buttonVal )
498 - .click( callback );
499 -
500 - $promptTextbox
501 - .keyup( function( e ) {
502 - if ( e.keyCode === 13 ) {
503 - $addButton.click();
504 - }
505 - } )
506 - .suggestions( {
507 - fetch: fetchSuggestions,
508 - cancel: function() {
509 - var req = this.data( 'suggestions-request' );
510 - // XMLHttpRequest.abort is unimplemented in IE6, also returns nonstandard value of 'unknown' for typeof
511 - if ( req && typeof req.abort !== 'unknown' && typeof req.abort !== 'undefined' && req.abort ) {
512 - req.abort();
513 - }
514 - }
515 - } )
516 - .suggestions();
517 -
518 - $promptContainer
519 - .append( $promptTextbox )
520 - .append( $addButton );
521 -
522 - return $promptContainer;
523 - },
524 -
525 - /**
526 - * Execute or queue a category addition.
527 - *
528 - * @param $link {jQuery} Anchor tag of category link inside #catlinks.
529 - * @param catTitle {mw.Title} Instance of mw.Title of the category to be added.
530 - * @param catSortkey {String} sort key (optional)
531 - * @param noAppend
532 - * @return {mw.ajaxCategories}
533 - */
534 - handleCategoryAdd: function( $link, catTitle, catSortkey, noAppend ) {
535 - var ajaxcat = this,
536 - // Suffix is wikitext between '[[Category:Foo' and ']]'.
537 - suffix = catSortkey ? '|' + catSortkey : '',
538 - catName = catTitle.getMainText(),
539 - catFull = catTitle.toText();
540 -
541 - if ( this.containsCat( catName ) ) {
542 - this.showError( mw.msg( 'ajax-category-already-present', catName ) );
543 - return this;
544 - }
545 -
546 - if ( !$link.length ) {
547 - $link = this.createCatLink( catTitle );
548 - }
549 -
550 - // Mark red if missing
551 - $link[(catTitle.exists() === false ? 'addClass' : 'removeClass')]( 'new' );
552 -
553 - this.doConfirmEdit( {
554 - modFn: function( oldText ) {
555 - var newText = ajaxcat.runHooks( oldText, 'beforeAdd', catName );
556 - newText = newText + "\n[[" + catFull + suffix + "]]\n";
557 - return ajaxcat.runHooks( newText, 'afterAdd', catName );
558 - },
559 - dialogDescription: mw.message( 'ajax-add-category-summary', catName ).escaped(),
560 - editSummary: '+[[' + catFull + ']]',
561 - doneFn: function( unsaved ) {
562 - if ( !noAppend ) {
563 - ajaxcat.options.$container
564 - .find( '#mw-normal-catlinks > .mw-addcategory-prompt' ).children( 'input' ).hide();
565 - ajaxcat.options.$container
566 - .find( '#mw-normal-catlinks ul' ).append( $link.parent() );
567 - } else {
568 - // Remove input box & button
569 - $link.data( 'deleteButton' ).click();
570 -
571 - // Update link text and href
572 - $link.show().text( catName ).attr( 'href', catTitle.getUrl() );
573 - }
574 - if ( unsaved ) {
575 - $link.addClass( 'mw-added-category' );
576 - }
577 - $( '.mw-ajax-addcategory' ).click();
578 - },
579 - $link: $link,
580 - action: 'add'
581 - } );
582 - return this;
583 - },
584 -
585 - /**
586 - * Execute or queue a category edit.
587 - *
588 - * @param $link {jQuery} Anchor tag of category link in #catlinks.
589 - * @param oldCatName {String} Name of category before edit
590 - * @param catTitle {mw.Title} Instance of mw.Title for new category
591 - * @param catSortkey {String} Sort key of new category link (optional)
592 - * @param isAdded {Boolean} True if this is a new link, false if it changed an existing one
593 - */
594 - handleCategoryEdit: function( $link, oldCatName, catTitle, catSortkey, isAdded ) {
595 - var ajaxcat = this,
596 - catName = catTitle.getMainText();
597 -
598 - // Category add needs to be handled differently
599 - if ( isAdded ) {
600 - // Pass sortkey back
601 - this.handleCategoryAdd( $link, catTitle, catSortkey, true );
602 - return;
603 - }
604 -
605 - // User didn't change anything, trigger delete
606 - // @todo Document why it's deleted.
607 - if ( oldCatName === catName ) {
608 - $link.data( 'deleteButton' ).click();
609 - return;
610 - }
611 -
612 - // Mark red if missing
613 - $link[(catTitle.exists() === false ? 'addClass' : 'removeClass')]( 'new' );
614 -
615 - var categoryRegex = buildRegex( oldCatName ),
616 - editSummary = '[[' + new mw.Title( oldCatName, catNsId ).toText() + ']] -> [[' + catTitle.toText() + ']]';
617 -
618 - ajaxcat.doConfirmEdit({
619 - modFn: function( oldText ) {
620 - var newText = ajaxcat.runHooks( oldText, 'beforeChange', oldCatName, catName ),
621 - matches = newText.match( categoryRegex );
622 -
623 - // Old cat wasn't found, likely to be transcluded
624 - if ( !$.isArray( matches ) ) {
625 - ajaxcat.showError( mw.msg( 'ajax-edit-category-error' ) );
626 - return false;
627 - }
628 -
629 - var suffix = catSortkey ? '|' + catSortkey : matches[0].replace( categoryRegex, '$2' ),
630 - newCategoryWikitext = '[[' + catTitle + suffix + ']]';
631 -
632 - if ( matches.length > 1 ) {
633 - // The category is duplicated. Remove all but one match
634 - for ( var i = 1; i < matches.length; i++ ) {
635 - oldText = oldText.replace( matches[i], '' );
636 - }
637 - }
638 - newText = oldText.replace( categoryRegex, newCategoryWikitext );
639 -
640 - return ajaxcat.runHooks( newText, 'afterChange', oldCatName, catName );
641 - },
642 - dialogDescription: mw.message( 'ajax-edit-category-summary', oldCatName, catName ).escaped(),
643 - editSummary: editSummary,
644 - doneFn: function( unsaved ) {
645 - // Remove input box & button
646 - $link.data( 'deleteButton' ).click();
647 -
648 - // Update link text and href
649 - $link.show().text( catName ).attr( 'href', catTitle.getUrl() );
650 - if ( unsaved ) {
651 - $link.data( 'origCat', oldCatName ).addClass( 'mw-changed-category' );
652 - }
653 - },
654 - $link: $link,
655 - action: 'edit'
656 - });
657 - },
658 -
659 - /**
660 - * Checks the API whether the category in question is a redirect.
661 - * Also returns existance info (to color link red/blue)
662 - * @param category {String} Name of category to resolve
663 - * @param callback {Function} Called with 1 argument (mw.Title object)
664 - */
665 - resolveRedirects: function( category, callback ) {
666 - if ( !this.options.resolveRedirects ) {
667 - callback( category, true );
668 - return;
669 - }
670 - var catTitle = new mw.Title( category, catNsId ),
671 - queryVars = {
672 - action:'query',
673 - titles: catTitle.toString(),
674 - redirects: 1,
675 - format: 'json'
676 - };
677 -
678 - $.getJSON( mw.util.wikiScript( 'api' ), queryVars, function( json ) {
679 - var redirect = json.query.redirects,
680 - exists = !json.query.pages[-1];
681 -
682 - // If it's a redirect 'exists' is for the target, not the origin
683 - if ( redirect ) {
684 - // Register existance of redirect origin as well,
685 - // a non-existent page can't be a redirect.
686 - mw.Title.exist.set( catTitle.toString(), true );
687 -
688 - // Override title with the redirect target
689 - catTitle = new mw.Title( redirect[0].to ).getMainText();
690 - }
691 -
692 - // Register existence
693 - mw.Title.exist.set( catTitle.toString(), exists );
694 -
695 - callback( catTitle );
696 - } );
697 - },
698 -
699 - /**
700 - * Append edit and remove buttons to a given category link
701 - *
702 - * @param DOMElement element Anchor element, to which the buttons should be appended.
703 - * @return {mw.ajaxCategories}
704 - */
705 - createCatButtons: function( $element ) {
706 - var deleteButton = createButton( 'icon-close', mw.msg( 'ajax-remove-category' ) ),
707 - editButton = createButton( 'icon-edit', mw.msg( 'ajax-edit-category' ) ),
708 - saveButton = createButton( 'icon-tick', mw.msg( 'ajax-confirm-save' ) ).hide(),
709 - ajaxcat = this;
710 -
711 - deleteButton.click( this.handleDeleteLink );
712 - editButton.click( ajaxcat.createEditInterface );
713 -
714 - $element.after( deleteButton ).after( editButton );
715 -
716 - // Save references to all links and buttons
717 - $element.data( {
718 - deleteButton: deleteButton,
719 - editButton: editButton,
720 - saveButton: saveButton
721 - } );
722 - editButton.data( {
723 - link: $element
724 - } );
725 - return this;
726 - },
727 -
728 - /**
729 - * Append spinner wheel to element.
730 - * @param $el {jQuery}
731 - * @return {mw.ajaxCategories}
732 - */
733 - addProgressIndicator: function( $el ) {
734 - $el.append( $( '<div>' ).addClass( 'mw-ajax-loader' ) );
735 - return this;
736 - },
737 -
738 - /**
739 - * Find and remove spinner wheel from inside element.
740 - * @param $el {jQuery}
741 - * @return {mw.ajaxCategories}
742 - */
743 - removeProgressIndicator: function( $el ) {
744 - $el.find( '.mw-ajax-loader' ).remove();
745 - return this;
746 - },
747 -
748 - /**
749 - * Parse the DOM $container and build a list of
750 - * present categories.
751 - *
752 - * @return {Array} All categories.
753 - */
754 - getCats: function() {
755 - var cats = this.options.$container
756 - .find( this.options.categoryLinkSelector )
757 - .map( function() {
758 - return $.trim( $( this ).text() );
759 - } );
760 - return cats;
761 - },
762 -
763 - /**
764 - * Check whether a passed category is present in the DOM.
765 - *
766 - * @param newCat {String} Category name to be checked for.
767 - * @return {Boolean}
768 - */
769 - containsCat: function( newCat ) {
770 - newCat = $.ucFirst( newCat );
771 - var match = false;
772 - $.each( this.getCats(), function(i, cat) {
773 - if ( $.ucFirst( cat ) === newCat ) {
774 - match = true;
775 - // Stop once we have a match
776 - return false;
777 - }
778 - } );
779 - return match;
780 - },
781 -
782 - /**
783 - * Execute or queue a category delete.
784 - *
785 - * @param $link {jQuery}
786 - * @param category
787 - * @return ?
788 - */
789 - handleCategoryDelete: function( $link, category ) {
790 - var categoryRegex = buildRegex( category, true ),
791 - ajaxcat = this;
792 -
793 - this.doConfirmEdit({
794 - modFn: function( oldText ) {
795 - var newText = ajaxcat.runHooks( oldText, 'beforeDelete', category );
796 - newText = newText.replace( categoryRegex, '' );
797 -
798 - if ( newText === oldText ) {
799 - ajaxcat.showError( mw.msg( 'ajax-remove-category-error' ) );
800 - return false;
801 - }
802 -
803 - return ajaxcat.runHooks( newText, 'afterDelete', category );
804 - },
805 - dialogDescription: mw.message( 'ajax-remove-category-summary', category ).escaped(),
806 - editSummary: '-[[' + new mw.Title( category, catNsId ) + ']]',
807 - doneFn: function( unsaved ) {
808 - if ( unsaved ) {
809 - $link.addClass( 'mw-removed-category' );
810 - } else {
811 - $link.parent().remove();
812 - }
813 - },
814 - $link: $link,
815 - action: 'delete'
816 - });
817 - },
818 -
819 - /**
820 - * Takes a category link element
821 - * and strips all data from it.
822 - *
823 - * @param $link {jQuery}
824 - * @param del {Boolean}
825 - * @param dontRestoreText {Boolean}
826 - * @return ?
827 - */
828 - resetCatLink: function( $link, del, dontRestoreText ) {
829 - $link.removeClass( 'mw-removed-category mw-added-category mw-changed-category' );
830 - var data = $link.data();
831 -
832 - if ( typeof data.stashIndex === 'number' ) {
833 - this.removeStashItem( data.stashIndex );
834 - }
835 - if ( del ) {
836 - $link.parent().remove();
837 - return;
838 - }
839 - if ( data.origCat && !dontRestoreText ) {
840 - var catTitle = new mw.Title( data.origCat, catNsId );
841 - $link.text( catTitle.getMainText() );
842 - $link.attr( 'href', catTitle.getUrl() );
843 - }
844 -
845 - $link.removeData();
846 -
847 - // Re-add data
848 - $link.data( {
849 - saveButton: data.saveButton,
850 - deleteButton: data.deleteButton,
851 - editButton: data.editButton
852 - } );
853 - },
854 -
855 - /**
856 - * Do the actual edit.
857 - * Gets token & text from api, runs it through fn
858 - * and saves it with summary.
859 - * @param page {String} Pagename
860 - * @param fn {Function} edit function
861 - * @param summary {String}
862 - * @param doneFn {String} Callback after all is done
863 - */
864 - doEdit: function( page, fn, summary, doneFn ) {
865 - // Get an edit token for the page.
866 - var getTokenVars = {
867 - action: 'query',
868 - prop: 'info|revisions',
869 - intoken: 'edit',
870 - titles: page,
871 - rvprop: 'content|timestamp',
872 - format: 'json'
873 - }, ajaxcat = this;
874 -
875 - $.post(
876 - mw.util.wikiScript( 'api' ),
877 - getTokenVars,
878 - function( json ) {
879 - if ( 'error' in json ) {
880 - ajaxcat.showError( mw.msg( 'ajax-api-error', json.error.code, json.error.info ) );
881 - return;
882 - } else if ( json.query && json.query.pages ) {
883 - var infos = json.query.pages;
884 - } else {
885 - ajaxcat.showError( mw.msg( 'ajax-api-unknown-error' ) );
886 - return;
887 - }
888 -
889 - $.each( infos, function( pageid, data ) {
890 - var token = data.edittoken,
891 - timestamp = data.revisions[0].timestamp,
892 - oldText = data.revisions[0]['*'],
893 - nowikiKey = generateRandomId(), // Unique ID for nowiki replacement
894 - nowikiFragments = []; // Nowiki fragments will be stored here during the changes
895 -
896 - // Replace all nowiki parts with unique keys..
897 - oldText = replaceNowikis( oldText, nowikiKey, nowikiFragments );
898 -
899 - // ..then apply the changes to the page text..
900 - var newText = fn( oldText );
901 - if ( newText === false ) {
902 - return;
903 - }
904 -
905 - // ..and restore the nowiki parts back.
906 - newText = restoreNowikis( newText, nowikiKey, nowikiFragments );
907 -
908 - var postEditVars = {
909 - action: 'edit',
910 - title: page,
911 - text: newText,
912 - summary: summary,
913 - token: token,
914 - basetimestamp: timestamp,
915 - format: 'json'
916 - };
917 -
918 - $.post(
919 - mw.util.wikiScript( 'api' ),
920 - postEditVars,
921 - doneFn,
922 - 'json'
923 - )
924 - .error( function( xhr, text, error ) {
925 - ajaxcat.showError( mw.msg( 'ajax-api-error', text, error ) );
926 - });
927 - } );
928 - },
929 - 'json'
930 - ).error( function( xhr, text, error ) {
931 - ajaxcat.showError( mw.msg( 'ajax-api-error', text, error ) );
932 - } );
933 - },
934 -
935 - /**
936 - * This gets called by all action buttons
937 - * Displays a dialog to confirm the action
938 - * Afterwards do the actual edit.
939 - *
940 - * @param props {Object}:
941 - * - modFn {Function} text-modifying function
942 - * - dialogDescription {String} Changes done (HTML for in the dialog, escape before hand if needed)
943 - * - editSummary {String} Changes done (text for the edit summary)
944 - * - doneFn {Function} callback after everything is done
945 - * - $link {jQuery}
946 - * - action
947 - * @return {mw.ajaxCategories}
948 - */
949 - doConfirmEdit: function( props ) {
950 - var summaryHolder, reasonBox, dialog, submitFunction,
951 - buttons = {},
952 - dialogOptions = {
953 - AutoOpen: true,
954 - buttons: buttons,
955 - width: 450
956 - },
957 - ajaxcat = this;
958 -
959 - // Check whether to use multiEdit mode:
960 - if ( this.options.multiEdit && props.action !== 'all' ) {
961 -
962 - // Stash away
963 - props.$link
964 - .data( 'stashIndex', this.stash.fns.length )
965 - .data( 'summary', props.dialogDescription );
966 -
967 - this.stash.dialogDescriptions.push( props.dialogDescription );
968 - this.stash.editSummaries.push( props.editSummary );
969 - this.stash.fns.push( props.modFn );
970 -
971 - this.saveAllButton.show();
972 - this.cancelAllButton.show();
973 -
974 - // Clear input field after action
975 - ajaxcat.addContainer.find( '.mw-addcategory-input' ).val( '' );
976 -
977 - // This only does visual changes, fire done and return.
978 - props.doneFn( true );
979 - return this;
980 - }
981 -
982 - // Summary of the action to be taken
983 - summaryHolder = $( '<p>' )
984 - .html( '<strong>' + mw.message( 'ajax-category-question' ).escaped() + '</strong><br/>' + props.dialogDescription );
985 -
986 - // Reason textbox.
987 - reasonBox = $( '<input type="text" size="45"></input>' )
988 - .addClass( 'mw-ajax-confirm-reason' );
989 -
990 - // Produce a confirmation dialog
991 - dialog = $( '<div>' )
992 - .addClass( 'mw-ajax-confirm-dialog' )
993 - .attr( 'title', mw.msg( 'ajax-confirm-title' ) )
994 - .append( summaryHolder )
995 - .append( reasonBox );
996 -
997 - // Submit button
998 - submitFunction = function() {
999 - ajaxcat.addProgressIndicator( dialog );
1000 - ajaxcat.doEdit(
1001 - mw.config.get( 'wgPageName' ),
1002 - props.modFn,
1003 - props.editSummary + ': ' + reasonBox.val(),
1004 - function() {
1005 - props.doneFn();
1006 -
1007 - // Clear input field after successful edit
1008 - ajaxcat.addContainer.find( '.mw-addcategory-input' ).val( '' );
1009 -
1010 - dialog.dialog( 'close' );
1011 - ajaxcat.removeProgressIndicator( dialog );
1012 - }
1013 - );
1014 - };
1015 -
1016 - buttons[mw.msg( 'ajax-confirm-save' )] = submitFunction;
1017 -
1018 - dialog.dialog( dialogOptions ).keyup( function( e ) {
1019 - // Close on enter
1020 - if ( e.keyCode === 13 ) {
1021 - submitFunction();
1022 - }
1023 - } );
1024 -
1025 - return this;
1026 - },
1027 -
1028 - /**
1029 - * @param index {Number|jQuery} Stash index or jQuery object of stash item.
1030 - * @return {mw.ajaxCategories}
1031 - */
1032 - removeStashItem: function( i ) {
1033 - if ( typeof i !== 'number' ) {
1034 - i = i.data( 'stashIndex' );
1035 - }
1036 -
1037 - try {
1038 - delete this.stash.fns[i];
1039 - delete this.stash.dialogDescriptions[i];
1040 - } catch(e) {}
1041 -
1042 - if ( $.isEmpty( this.stash.fns ) ) {
1043 - this.stash.fns = [];
1044 - this.stash.dialogDescriptions = [];
1045 - this.stash.editSummaries = [];
1046 - this.saveAllButton.hide();
1047 - this.cancelAllButton.hide();
1048 - }
1049 - return this;
1050 - },
1051 -
1052 - /**
1053 - * Reset all data from the category links and the stash.
1054 - *
1055 - * @param del {Boolean} Delete any category links with .mw-removed-category
1056 - * @return {mw.ajaxCategories}
1057 - */
1058 - resetAll: function( del ) {
1059 - var $links = this.options.$container.find( this.options.categoryLinkSelector ),
1060 - $del = $([]),
1061 - ajaxcat = this;
1062 -
1063 - if ( del ) {
1064 - $del = $links.filter( '.mw-removed-category' ).parent();
1065 - }
1066 -
1067 - $links.each( function() {
1068 - ajaxcat.resetCatLink( $( this ), false, del );
1069 - } );
1070 -
1071 - $del.remove();
1072 -
1073 - this.options.$container.find( '#mw-hidden-catlinks' ).remove();
1074 -
1075 - return this;
1076 - },
1077 -
1078 - /**
1079 - * Add hooks
1080 - * Currently available: beforeAdd, beforeChange, beforeDelete,
1081 - * afterAdd, afterChange, afterDelete
1082 - * If the hook function returns false, all changes are aborted.
1083 - *
1084 - * @param string type Type of hook to add
1085 - * @param function fn Hook function. The following vars are passed to it:
1086 - * 1. oldtext: The wikitext before the hook
1087 - * 2. category: The deleted, added, or changed category
1088 - * 3. (only for beforeChange/afterChange): newcategory
1089 - */
1090 - addHook: function( type, fn ) {
1091 - if ( !this.hooks[type] || !$.isFunction( fn ) ) {
1092 - return;
1093 - }
1094 - else {
1095 - this.hooks[type].push( fn );
1096 - }
1097 - },
1098 -
1099 -
1100 - /**
1101 - * Open a dismissable error dialog
1102 - *
1103 - * @param string str The error description
1104 - */
1105 - showError: function( str ) {
1106 - var oldDialog = $( '.mw-ajax-confirm-dialog' ),
1107 - buttons = {},
1108 - dialogOptions = {
1109 - buttons: buttons,
1110 - AutoOpen: true,
1111 - title: mw.msg( 'ajax-error-title' )
1112 - };
1113 -
1114 - this.removeProgressIndicator( oldDialog );
1115 - oldDialog.dialog( 'close' );
1116 -
1117 - var dialog = $( '<div>' ).text( str );
1118 -
1119 - mw.util.$content.append( dialog );
1120 -
1121 - buttons[mw.msg( 'ajax-confirm-ok' )] = function( e ) {
1122 - dialog.dialog( 'close' );
1123 - };
1124 -
1125 - dialog.dialog( dialogOptions ).keyup( function( e ) {
1126 - if ( e.keyCode === 13 ) {
1127 - dialog.dialog( 'close' );
1128 - }
1129 - } );
1130 - },
1131 -
1132 - /**
1133 - * @param oldtext
1134 - * @param type
1135 - * @param category
1136 - * @param categoryNew
1137 - * @return oldtext
1138 - */
1139 - runHooks: function( oldtext, type, category, categoryNew ) {
1140 - // No hooks registered
1141 - if ( !this.hooks[type] ) {
1142 - return oldtext;
1143 - } else {
1144 - for ( var i = 0; i < this.hooks[type].length; i++ ) {
1145 - oldtext = this.hooks[type][i]( oldtext, category, categoryNew );
1146 - if ( oldtext === false ) {
1147 - this.showError( mw.msg( 'ajax-category-hook-error', category ) );
1148 - return;
1149 - }
1150 - }
1151 - return oldtext;
1152 - }
1153 - }
1154 -};
1155 -
1156 -} )( jQuery );
Index: trunk/phase3/resources/mediawiki.page/mediawiki.page.ajaxCategories.init.js
@@ -1,6 +0,0 @@
2 -mw.page.ajaxCategories = new mw.ajaxCategories();
3 -jQuery( document ).ready( function(){
4 - // Separate function for call to prevent jQuery
5 - // from executing it in the document context.
6 - mw.page.ajaxCategories.setup();
7 -} );
Index: trunk/phase3/resources/mediawiki.page/mediawiki.page.ajaxCategories.css
@@ -1,70 +0,0 @@
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 -.mw-removed-category {
16 - text-decoration: line-through;
17 -}
18 -
19 -#catlinks:hover .icon {
20 - opacity: 1;
21 -}
22 -#catlinks ul {
23 - margin-right: 2em;
24 -}
25 -
26 -.mw-ajax-addcategory-holder {
27 - display: inline-block;
28 -}
29 -.mw-ajax-addcategory {
30 - margin-right: 1em;
31 - cursor: pointer;
32 - display: inline-block;
33 -}
34 -
35 -#catlinks .icon {
36 - cursor: pointer;
37 - padding: 1px 8px;
38 - margin: 0;
39 - background-position: 0 0;
40 - background-repeat: no-repeat;
41 - opacity: 0.5;
42 -
43 -}
44 -#catlinks .icon-parent {
45 - cursor: pointer;
46 - margin-right: 1em;
47 -}
48 -#catlinks .icon-close {
49 - /* @embed */ background-image: url(images/ajaxcat-close.png);
50 -}
51 -#catlinks .icon-edit {
52 - /* @embed */ background-image: url(images/ajaxcat-edit.png);
53 -}
54 -#catlinks .icon-tick {
55 - /* @embed */ background-image: url(images/ajaxcat-tick.png);
56 -}
57 -#catlinks .icon-add {
58 - /* @embed */ background-image: url(images/ajaxcat-add.png);
59 -}
60 -#catlinks .icon-close:hover {
61 - /* @embed */ background-image: url(images/ajaxcat-close-hover.png);
62 -}
63 -#catlinks .icon-edit:hover {
64 - /* @embed */ background-image: url(images/ajaxcat-edit-hover.png);
65 -}
66 -#catlinks .icon-tick:hover {
67 - /* @embed */ background-image: url(images/ajaxcat-tick-hover.png);
68 -}
69 -#catlinks .icon-add:hover {
70 - /* @embed */ background-image: url(images/ajaxcat-add-hover.png);
71 -}
Index: trunk/extensions/InlineCategorizer/InlineCategorizer.i18n.php
@@ -0,0 +1,68 @@
 2+<?php
 3+/**
 4+ * Internationalisation for InlineCategorizer extension
 5+ *
 6+ * @file
 7+ * @ingroup Extensions
 8+ */
 9+
 10+$messages = array();
 11+
 12+/** English
 13+ * @author Nimish Gautam
 14+ */
 15+$messages['en'] = array(
 16+ 'inlinecategorizer-desc' => 'JavaScript module that enables changing, adding and removing categorylinks directly from a page.',
 17+
 18+ // Ajax interface
 19+ 'inlinecategorizer-add-category' => 'Add category',
 20+ 'inlinecategorizer-add-category-submit' => 'Add',
 21+ 'inlinecategorizer-add-category-summary' => 'Add category "$1"',
 22+ 'inlinecategorizer-api-error' => 'The API returned an error: $1: $2.',
 23+ 'inlinecategorizer-api-unknown-error' => 'The API returned an unknown error.',
 24+ 'inlinecategorizer-cancel' => 'Cancel edit',
 25+ 'inlinecategorizer-cancel-all' => 'Cancel all changes',
 26+ 'inlinecategorizer-category-already-present' => 'This page already belongs to the category "$1"',
 27+ 'inlinecategorizer-category-hook-error' => 'A local function prevented the changes from being saved.',
 28+ 'inlinecategorizer-category-question' => 'Why do you want to make the following changes:',
 29+ 'inlinecategorizer-confirm-ok' => 'OK',
 30+ 'inlinecategorizer-confirm-save' => 'Save',
 31+ 'inlinecategorizer-confirm-save-all' => 'Save all changes',
 32+ 'inlinecategorizer-confirm-title' => 'Confirm action',
 33+ 'inlinecategorizer-edit-category' => 'Edit category',
 34+ 'inlinecategorizer-edit-category-error' => 'It was not possible to edit category "$1".
 35+This usually occurs when the category has been added to the page in a template.',
 36+ 'inlinecategorizer-edit-category-summary' => 'Change category "$1" to "$2"',
 37+ 'inlinecategorizer-error-title' => 'Error',
 38+ 'inlinecategorizer-remove-category' => 'Remove category',
 39+ 'inlinecategorizer-remove-category-error' => 'It was not possible to remove category "$1".
 40+This usually occurs when the category has been added to the page in a template.',
 41+ 'inlinecategorizer-remove-category-summary' => 'Remove category "$1"',
 42+);
 43+
 44+/** Message documentation (Message documentation)
 45+ * @author EugeneZelenko
 46+ * @author Fryed-peach
 47+ */
 48+$messages['qqq'] = array(
 49+ 'inlinecategorizer-desc' => '{{desc}}',
 50+
 51+ // Ajax interface
 52+ 'inlinecategorizer-add-category-submit' => '{{Identical|Add}}',
 53+ 'inlinecategorizer-add-category-summary' => 'See {{msg-mw|inlinecategorizer-category-question}}. $1 is a category name.',
 54+ 'inlinecategorizer-api-error' => 'API = [http://en.wikipedia.org/wiki/Application_programming_interface Application programming interface].
 55+
 56+"returned" here means "reported".',
 57+ 'inlinecategorizer-category-already-present' => 'Error message. $1 is the category name',
 58+ 'inlinecategorizer-category-question' => "Question the user is asked before submit. It's followed by a list of the changes.",
 59+ 'inlinecategorizer-confirm-ok' => '{{Identical|OK}}',
 60+ 'inlinecategorizer-confirm-save' => 'Submit button {{Identical|Save}}',
 61+ 'inlinecategorizer-confirm-save-all' => 'Submit button to save all changes',
 62+ 'inlinecategorizer-confirm-title' => 'Title for a dialog box in which the user is asked for an edit summary',
 63+ 'inlinecategorizer-edit-category' => 'Tooltip for the edit link displayed after each category at the foot of a page. Refers to the specific category. "Edit this category" is also correct.',
 64+ 'inlinecategorizer-edit-category-summary' => 'See {{msg-mw|inlinecategorizer-category-question}}. $1 and $2 are both category names.',
 65+ 'inlinecategorizer-error-title' => '{{Identical|Error}}',
 66+ 'inlinecategorizer-remove-category' => 'Tooltip for link to remove a category from the page, displayed after each category at the foot of a page. Refers to the specific category. "Remove this category" is also correct.',
 67+ 'inlinecategorizer-remove-category-summary' => 'See {{msg-mw|inlinecategorizer-category-question}}. $1 is a category name.',
 68+);
 69+
\ No newline at end of file
Index: trunk/extensions/InlineCategorizer/InlineCategorizer.php
@@ -0,0 +1,90 @@
 2+<?php
 3+/**
 4+ * InlineCategorizer extension
 5+ *
 6+ * @file
 7+ * @ingroup Extensions
 8+ *
 9+ * @author Timo Tijhof <ttijhof@wikimedia.org>
 10+ * @license GPL v2 or later
 11+ * @version 0.0.1
 12+ */
 13+
 14+/* Configuration */
 15+
 16+/**
 17+ * Optinally enable InlineCategorizer only on a set of namespaces.
 18+ * Default is all.
 19+ *
 20+ * Example:
 21+ * $wgInlineCategorizerNamespaces = array( NS_MAIN, NS_PROJECT );
 22+ */
 23+$wgInlineCategorizerNamespaces = array();
 24+
 25+/* Setup */
 26+
 27+$wgExtensionCredits['other'][] = array(
 28+ 'path' => __FILE__,
 29+ 'name' => 'InlineCategorizer',
 30+ 'author' => array(
 31+ 'Michael Dale',
 32+ 'Timo Tijhof',
 33+ 'Leo Koppelkamm',
 34+ ),
 35+ 'version' => '0.0.1',
 36+ 'descriptionmsg' => 'inlinecategorizer-desc',
 37+ 'url' => 'http://www.mediawiki.org/wiki/Extension:InlineCategorizer'
 38+);
 39+
 40+// Autoloading
 41+$dir = dirname( __FILE__ ) . '/';
 42+$wgAutoloadClasses['InlineCategorizerHooks'] = $dir . 'InlineCategorizer.hooks.php';
 43+$wgExtensionMessagesFiles['InlineCategorizer'] = $dir . 'InlineCategorizer.i18n.php';
 44+
 45+// Hooks
 46+$wgHooks['BeforePageDisplay'][] = 'InlineCategorizerHooks::beforePageDisplay';
 47+
 48+// Modules
 49+$commonModuleInfo = array(
 50+ 'localBasePath' => dirname( __FILE__ ) . '/modules',
 51+ 'remoteExtPath' => 'InlineCategorizer/modules',
 52+);
 53+
 54+$wgResourceModules['ext.inlineCategorizer.core'] = array(
 55+ 'scripts' => 'ext.inlineCategorizer.core.js',
 56+ 'styles' => 'ext.inlineCategorizer.core.css',
 57+ 'dependencies' => array(
 58+ 'jquery.suggestions',
 59+ 'jquery.ui.dialog',
 60+ 'mediawiki.Title',
 61+ ),
 62+ 'messages' => array(
 63+ 'inlinecategorizer-add-category',
 64+ 'inlinecategorizer-remove-category',
 65+ 'inlinecategorizer-edit-category',
 66+ 'inlinecategorizer-add-category-submit',
 67+ 'inlinecategorizer-confirm-ok',
 68+ 'inlinecategorizer-confirm-title',
 69+ 'inlinecategorizer-confirm-save',
 70+ 'inlinecategorizer-confirm-save-all',
 71+ 'inlinecategorizer-cancel',
 72+ 'inlinecategorizer-cancel-all',
 73+ 'inlinecategorizer-add-category-summary',
 74+ 'inlinecategorizer-edit-category-summary',
 75+ 'inlinecategorizer-remove-category-summary',
 76+ 'inlinecategorizer-category-question',
 77+ 'inlinecategorizer-category-and',
 78+ 'inlinecategorizer-error-title',
 79+ 'inlinecategorizer-remove-category-error',
 80+ 'inlinecategorizer-edit-category-error',
 81+ 'inlinecategorizer-category-already-present',
 82+ 'inlinecategorizer-category-hook-error',
 83+ 'inlinecategorizer-api-error',
 84+ 'inlinecategorizer-api-unknown-error',
 85+ ),
 86+) + $commonModuleInfo;
 87+
 88+$wgResourceModules['ext.inlineCategorizer.init'] = array(
 89+ 'scripts' => 'ext.inlineCategorizer.init.js',
 90+ 'dependencies' => 'ext.inlineCategorizer.core',
 91+) + $commonModuleInfo;
Index: trunk/extensions/InlineCategorizer/InlineCategorizer.hooks.php
@@ -0,0 +1,27 @@
 2+<?php
 3+/**
 4+ * Hooks for InlineCategorizer
 5+ *
 6+ * @file
 7+ * @ingroup Extensions
 8+ */
 9+
 10+class InlineCategorizerHooks {
 11+
 12+ /**
 13+ * BeforePageDisplay hook
 14+ */
 15+ public static function beforePageDisplay( $out ) {
 16+ global $wgInlineCategorizerNamespaces;
 17+
 18+ // Only load if there are no restrictions, or if the current namespace
 19+ // is in the array.
 20+ if ( count( $wgInlineCategorizerNamespaces ) === 0
 21+ || in_array( $out->getTitle()->getNamespace(), $wgInlineCategorizerNamespaces )
 22+ ) {
 23+ $out->addModules( 'ext.inlineCategorizer.init' );
 24+ }
 25+ return true;
 26+ }
 27+
 28+}
Index: trunk/extensions/InlineCategorizer/modules/images/ajaxcat-close.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: trunk/extensions/InlineCategorizer/modules/images/ajaxcat-close.png
___________________________________________________________________
Added: svn:mime-type
129 + image/png
Index: trunk/extensions/InlineCategorizer/modules/images/ajaxcat-edit.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: trunk/extensions/InlineCategorizer/modules/images/ajaxcat-edit.png
___________________________________________________________________
Added: svn:mime-type
230 + image/png
Index: trunk/extensions/InlineCategorizer/modules/images/ajaxcat-close-hover.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: trunk/extensions/InlineCategorizer/modules/images/ajaxcat-close-hover.png
___________________________________________________________________
Added: svn:mime-type
331 + image/png
Index: trunk/extensions/InlineCategorizer/modules/images/ajaxcat-edit-hover.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: trunk/extensions/InlineCategorizer/modules/images/ajaxcat-edit-hover.png
___________________________________________________________________
Added: svn:mime-type
432 + image/png
Index: trunk/extensions/InlineCategorizer/modules/images/ajaxcat-add.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: trunk/extensions/InlineCategorizer/modules/images/ajaxcat-add.png
___________________________________________________________________
Added: svn:mime-type
533 + image/png
Index: trunk/extensions/InlineCategorizer/modules/images/ajaxcat-add-hover.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: trunk/extensions/InlineCategorizer/modules/images/ajaxcat-add-hover.png
___________________________________________________________________
Added: svn:mime-type
634 + image/png
Index: trunk/extensions/InlineCategorizer/modules/images/ajaxcat-error.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: trunk/extensions/InlineCategorizer/modules/images/ajaxcat-error.png
___________________________________________________________________
Added: svn:mime-type
735 + image/png
Index: trunk/extensions/InlineCategorizer/modules/images/ajaxcat-error-hover.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: trunk/extensions/InlineCategorizer/modules/images/ajaxcat-error-hover.png
___________________________________________________________________
Added: svn:mime-type
836 + image/png
Index: trunk/extensions/InlineCategorizer/modules/images/ajaxcat-tick.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: trunk/extensions/InlineCategorizer/modules/images/ajaxcat-tick.png
___________________________________________________________________
Added: svn:mime-type
937 + image/png
Index: trunk/extensions/InlineCategorizer/modules/images/ajaxcat-tick-hover.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: trunk/extensions/InlineCategorizer/modules/images/ajaxcat-tick-hover.png
___________________________________________________________________
Added: svn:mime-type
1038 + image/png
Index: trunk/extensions/InlineCategorizer/modules/ext.InlineCategorizer.init.js
@@ -0,0 +1,9 @@
 2+/**
 3+ * Initialize an instance of InlineCategorizer into mw.page
 4+ */
 5+mw.page.inlineCategorizer = new mw.InlineCategorizer();
 6+jQuery( document ).ready( function(){
 7+ // Separate function for call to prevent jQuery
 8+ // from executing it in the document context.
 9+ mw.page.inlineCategorizer.setup();
 10+} );
Index: trunk/extensions/InlineCategorizer/modules/ext.inlineCategorizer.init.js
@@ -0,0 +1,9 @@
 2+/**
 3+ * Initialize an instance of InlineCategorizer into mw.page
 4+ */
 5+mw.page.inlineCategorizer = new mw.InlineCategorizer();
 6+jQuery( document ).ready( function(){
 7+ // Separate function for call to prevent jQuery
 8+ // from executing it in the document context.
 9+ mw.page.inlineCategorizer.setup();
 10+} );
Property changes on: trunk/extensions/InlineCategorizer/modules/ext.inlineCategorizer.init.js
___________________________________________________________________
Added: svn:eol-style
111 + native
Index: trunk/extensions/InlineCategorizer/modules/ext.InlineCategorizer.core.css
@@ -0,0 +1,70 @@
 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+.mw-removed-category {
 16+ text-decoration: line-through;
 17+}
 18+
 19+#catlinks:hover .icon {
 20+ opacity: 1;
 21+}
 22+#catlinks ul {
 23+ margin-right: 2em;
 24+}
 25+
 26+.mw-ajax-addcategory-holder {
 27+ display: inline-block;
 28+}
 29+.mw-ajax-addcategory {
 30+ margin-right: 1em;
 31+ cursor: pointer;
 32+ display: inline-block;
 33+}
 34+
 35+#catlinks .icon {
 36+ cursor: pointer;
 37+ padding: 1px 8px;
 38+ margin: 0;
 39+ background-position: 0 0;
 40+ background-repeat: no-repeat;
 41+ opacity: 0.5;
 42+
 43+}
 44+#catlinks .icon-parent {
 45+ cursor: pointer;
 46+ margin-right: 1em;
 47+}
 48+#catlinks .icon-close {
 49+ /* @embed */ background-image: url(images/ajaxcat-close.png);
 50+}
 51+#catlinks .icon-edit {
 52+ /* @embed */ background-image: url(images/ajaxcat-edit.png);
 53+}
 54+#catlinks .icon-tick {
 55+ /* @embed */ background-image: url(images/ajaxcat-tick.png);
 56+}
 57+#catlinks .icon-add {
 58+ /* @embed */ background-image: url(images/ajaxcat-add.png);
 59+}
 60+#catlinks .icon-close:hover {
 61+ /* @embed */ background-image: url(images/ajaxcat-close-hover.png);
 62+}
 63+#catlinks .icon-edit:hover {
 64+ /* @embed */ background-image: url(images/ajaxcat-edit-hover.png);
 65+}
 66+#catlinks .icon-tick:hover {
 67+ /* @embed */ background-image: url(images/ajaxcat-tick-hover.png);
 68+}
 69+#catlinks .icon-add:hover {
 70+ /* @embed */ background-image: url(images/ajaxcat-add-hover.png);
 71+}
Index: trunk/extensions/InlineCategorizer/modules/ext.inlineCategorizer.core.css
@@ -0,0 +1,70 @@
 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+.mw-removed-category {
 16+ text-decoration: line-through;
 17+}
 18+
 19+#catlinks:hover .icon {
 20+ opacity: 1;
 21+}
 22+#catlinks ul {
 23+ margin-right: 2em;
 24+}
 25+
 26+.mw-ajax-addcategory-holder {
 27+ display: inline-block;
 28+}
 29+.mw-ajax-addcategory {
 30+ margin-right: 1em;
 31+ cursor: pointer;
 32+ display: inline-block;
 33+}
 34+
 35+#catlinks .icon {
 36+ cursor: pointer;
 37+ padding: 1px 8px;
 38+ margin: 0;
 39+ background-position: 0 0;
 40+ background-repeat: no-repeat;
 41+ opacity: 0.5;
 42+
 43+}
 44+#catlinks .icon-parent {
 45+ cursor: pointer;
 46+ margin-right: 1em;
 47+}
 48+#catlinks .icon-close {
 49+ /* @embed */ background-image: url(images/ajaxcat-close.png);
 50+}
 51+#catlinks .icon-edit {
 52+ /* @embed */ background-image: url(images/ajaxcat-edit.png);
 53+}
 54+#catlinks .icon-tick {
 55+ /* @embed */ background-image: url(images/ajaxcat-tick.png);
 56+}
 57+#catlinks .icon-add {
 58+ /* @embed */ background-image: url(images/ajaxcat-add.png);
 59+}
 60+#catlinks .icon-close:hover {
 61+ /* @embed */ background-image: url(images/ajaxcat-close-hover.png);
 62+}
 63+#catlinks .icon-edit:hover {
 64+ /* @embed */ background-image: url(images/ajaxcat-edit-hover.png);
 65+}
 66+#catlinks .icon-tick:hover {
 67+ /* @embed */ background-image: url(images/ajaxcat-tick-hover.png);
 68+}
 69+#catlinks .icon-add:hover {
 70+ /* @embed */ background-image: url(images/ajaxcat-add-hover.png);
 71+}
Property changes on: trunk/extensions/InlineCategorizer/modules/ext.inlineCategorizer.core.css
___________________________________________________________________
Added: svn:eol-style
172 + native
Index: trunk/extensions/InlineCategorizer/modules/ext.InlineCategorizer.core.js
@@ -0,0 +1,1152 @@
 2+/**
 3+ * The core of InlineCategorizer
 4+ *
 5+ * @author Michael Dale, 2009
 6+ * @author Leo Koppelkamm, 2011
 7+ * @author Timo Tijhof, 2011
 8+ *
 9+ * Relies on: mw.config (wgFormattedNamespaces, wgNamespaceIds,
 10+ * wgCaseSensitiveNamespaces, wgUserGroups), mw.util.wikiGetlink
 11+ */
 12+( function( $ ) {
 13+
 14+ /* Local scope */
 15+
 16+ var catNsId = mw.config.get( 'wgNamespaceIds' ).category,
 17+ defaultOptions = {
 18+ catLinkWrapper: '<li>',
 19+ $container: $( '.catlinks' ),
 20+ $containerNormal: $( '#mw-normal-catlinks' ),
 21+ categoryLinkSelector: 'li a:not(.icon)',
 22+ multiEdit: $.inArray( 'user', mw.config.get( 'wgUserGroups' ) ) !== -1,
 23+ resolveRedirects: true
 24+ },
 25+ isCatNsSensitive = $.inArray( 14, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1;
 26+
 27+ /**
 28+ * @return {String}
 29+ */
 30+ function clean( s ) {
 31+ if ( typeof s === 'string' ) {
 32+ return s.replace( /[\x00-\x1f\x23\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f\s]+/g, '' );
 33+ }
 34+ return '';
 35+ }
 36+
 37+ /**
 38+ * Generates a random id out of 62 alpha-numeric characters.
 39+ *
 40+ * @param {Number} Length of id (optional, defaults to 32)
 41+ * @return {String}
 42+ */
 43+ function generateRandomId( idLength ) {
 44+ idLength = typeof idLength === 'number' ? idLength : 32;
 45+ var seed = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
 46+ id = '';
 47+ for ( var r, i = 0; i < idLength; i++ ) {
 48+ r = Math.floor( Math.random() * seed.length );
 49+ id += seed.substring( r, r + 1 );
 50+ }
 51+ return id;
 52+ }
 53+
 54+ /**
 55+ * Helper function for $.fn.suggestions
 56+ *
 57+ * @context {jQuery}
 58+ * @param value {String} Textbox value.
 59+ */
 60+ function fetchSuggestions( value ) {
 61+ var request,
 62+ $el = this,
 63+ catName = clean( value );
 64+
 65+ request = $.ajax( {
 66+ url: mw.util.wikiScript( 'api' ),
 67+ data: {
 68+ action: 'query',
 69+ list: 'allpages',
 70+ apnamespace: catNsId,
 71+ apprefix: catName,
 72+ format: 'json'
 73+ },
 74+ dataType: 'json',
 75+ success: function( data ) {
 76+ // Process data.query.allpages into an array of titles
 77+ var pages = data.query.allpages,
 78+ titleArr = $.map( pages, function( page ) {
 79+ return new mw.Title( page.title ).getMainText();
 80+ } );
 81+
 82+ $el.suggestions( 'suggestions', titleArr );
 83+ }
 84+ } );
 85+ $el.data( 'suggestions-request', request );
 86+ }
 87+
 88+ /**
 89+ * Replace <nowiki> and comments with unique keys in the page text.
 90+ *
 91+ * @param text {String}
 92+ * @param id {String} Unique key for this nowiki replacement layer call.
 93+ * @param keys {Array} Array where fragments will be stored in.
 94+ * @return {String}
 95+ */
 96+ function replaceNowikis( text, id, keys ) {
 97+ var matches = text.match( /(<nowiki\>[\s\S]*?<\/nowiki>|<\!--[\s\S]*?--\>)/g );
 98+ for ( var i = 0; matches && i < matches.length; i++ ) {
 99+ keys[i] = matches[i];
 100+ text = text.replace( matches[i], '' + id + '-' + i );
 101+ }
 102+ return text;
 103+ }
 104+
 105+ /**
 106+ * Restore <nowiki> and comments from unique keys in the page text.
 107+ *
 108+ * @param text {String}
 109+ * @param id {String} Unique key of the layer to be restored, as passed to replaceNowikis().
 110+ * @param keys {Array} Array where fragements should be fetched from.
 111+ * @return {String}
 112+ */
 113+ function restoreNowikis( text, id, keys ) {
 114+ for ( var i = 0; i < keys.length; i++ ) {
 115+ text = text.replace( '' + id + '-' + i, keys[i] );
 116+ }
 117+ return text;
 118+ }
 119+
 120+ /**
 121+ * Makes regex string caseinsensitive.
 122+ * Useful when 'i' flag can't be used.
 123+ * Return stuff like [Ff][Oo][Oo]
 124+ *
 125+ * @param string {String} Regex string
 126+ * @return {String} Processed regex string
 127+ */
 128+ function makeCaseInsensitive( string ) {
 129+ var newString = '';
 130+ for ( var i = 0; i < string.length; i++ ) {
 131+ newString += '[' + string.charAt( i ).toUpperCase() + string.charAt( i ).toLowerCase() + ']';
 132+ }
 133+ return newString;
 134+ }
 135+
 136+ /**
 137+ * Build a regex that matches legal invocations of the passed category.
 138+ * @param category {String}
 139+ * @param matchLineBreak {Boolean} Match one following linebreak as well?
 140+ * @return {RegExp}
 141+ */
 142+ function buildRegex( category, matchLineBreak ) {
 143+ var categoryRegex, categoryNSFragment,
 144+ titleFragment = $.escapeRE( category ).replace( /( |_)/g, '[ _]' ),
 145+ firstChar = titleFragment.charAt( 0 );
 146+
 147+ // Filter out all names for category namespace
 148+ categoryNSFragment = $.map( mw.config.get( 'wgNamespaceIds' ), function( id, name ) {
 149+ if ( id === catNsId ) {
 150+ name = $.escapeRE( name );
 151+ return !isCatNsSensitive ? makeCaseInsensitive( name ) : name;
 152+ }
 153+ // Otherwise don't include in categoryNSFragment
 154+ return null;
 155+ } ).join( '|' );
 156+
 157+ firstChar = '[' + firstChar.toUpperCase() + firstChar.toLowerCase() + ']';
 158+ titleFragment = firstChar + titleFragment.substr( 1 );
 159+ categoryRegex = '\\[\\[(' + categoryNSFragment + ')' + '[ _]*' + ':' + '[ _]*' + titleFragment + '[ _]*' + '(\\|[^\\]]*)?\\]\\]';
 160+ if ( matchLineBreak ) {
 161+ categoryRegex += '[ \\t\\r]*\\n?';
 162+ }
 163+ return new RegExp( categoryRegex, 'g' );
 164+ }
 165+
 166+ /**
 167+ * Manufacture iconed button, with or without text.
 168+ *
 169+ * @param icon {String} The icon class.
 170+ * @param title {String} Title attribute.
 171+ * @param className {String} (optional) Additional classes to be added to the button.
 172+ * @param text {String} (optional) Text label of button.
 173+ * @return {jQuery} The button.
 174+ */
 175+ function createButton( icon, title, className, text ){
 176+ // We're adding a zero width space for IE7, it's got problems with empty nodes apparently
 177+ var $button = $( '<a>' )
 178+ .addClass( className || '' )
 179+ .attr( 'title', title )
 180+ .html( '&#8203;' );
 181+
 182+ if ( text ) {
 183+ var $icon = $( '<span>' ).addClass( 'icon ' + icon ).html( '&#8203;' );
 184+ $button.addClass( 'icon-parent' ).append( $icon ).append( mw.html.escape( text ) );
 185+ } else {
 186+ $button.addClass( 'icon ' + icon );
 187+ }
 188+ return $button;
 189+ }
 190+
 191+/**
 192+ * mw.InlineCategorizer
 193+ *
 194+ * @constructor
 195+ * @param options {Object}
 196+ */
 197+mw.InlineCategorizer = function( options ) {
 198+
 199+ this.options = options = $.extend( defaultOptions, options );
 200+
 201+ // Save scope in shortcut
 202+ var ajaxcat = this;
 203+
 204+ // Elements tied to this instance
 205+ this.saveAllButton = null;
 206+ this.cancelAllButton = null;
 207+ this.addContainer = null;
 208+
 209+ this.request = null;
 210+
 211+ // Stash and hooks
 212+ this.stash = {
 213+ dialogDescriptions: [],
 214+ editSummaries: [],
 215+ fns: []
 216+ };
 217+ this.hooks = {
 218+ beforeAdd: [],
 219+ beforeChange: [],
 220+ beforeDelete: [],
 221+ afterAdd: [],
 222+ afterChange: [],
 223+ afterDelete: []
 224+ };
 225+
 226+ /* Event handlers */
 227+
 228+ /**
 229+ * Handle add category submit. Not to be called directly.
 230+ *
 231+ * @context Element
 232+ * @param e {jQuery Event}
 233+ */
 234+ this.handleAddLink = function( e ) {
 235+ var $el = $( this ),
 236+ $link = $([]),
 237+ categoryText = $.ucFirst( $el.parent().find( '.mw-addcategory-input' ).val() || '' );
 238+
 239+ // Resolve redirects
 240+ ajaxcat.resolveRedirects( categoryText, function( resolvedCatTitle ) {
 241+ ajaxcat.handleCategoryAdd( $link, resolvedCatTitle, '', false );
 242+ } );
 243+ };
 244+
 245+ /**
 246+ * @context Element
 247+ * @param e {jQuery Event}
 248+ */
 249+ this.createEditInterface = function( e ) {
 250+ var $el = $( this ),
 251+ $link = $el.data( 'link' ),
 252+ category = $link.text(),
 253+ $input = ajaxcat.makeSuggestionBox( category,
 254+ ajaxcat.handleEditLink,
 255+ ajaxcat.options.multiEdit ? mw.msg( 'inlinecategorizer-confirm-ok' ) : mw.msg( 'inlinecategorizer-confirm-save' )
 256+ );
 257+
 258+ $link.after( $input ).hide();
 259+
 260+ $input.find( '.mw-addcategory-input' ).focus();
 261+
 262+ // Get the editButton associated with this category link,
 263+ // and hide it.
 264+ $link.data( 'editButton' ).hide();
 265+
 266+ // Get the deleteButton associated with this category link,
 267+ $link.data( 'deleteButton' )
 268+ // (re)set click handler
 269+ .unbind( 'click' )
 270+ .click( function() {
 271+ // When the delete button is clicked:
 272+ // - Remove the suggestion box
 273+ // - Show the link and it's edit button
 274+ // - (re)set the click handler again
 275+ $input.remove();
 276+ $link.show().data( 'editButton' ).show();
 277+ $( this )
 278+ .unbind( 'click' )
 279+ .click( ajaxcat.handleDeleteLink )
 280+ .attr( 'title', mw.msg( 'inlinecategorizer-remove-category' ) );
 281+ })
 282+ .attr( 'title', mw.msg( 'inlinecategorizer-cancel' ) );
 283+ };
 284+
 285+ /**
 286+ * Handle edit category submit. Not to be called directly.
 287+ *
 288+ * @context Element
 289+ * @param e {jQuery Event}
 290+ */
 291+ this.handleEditLink = function( e ) {
 292+ var input, category, sortkey, categoryOld,
 293+ $el = $( this ),
 294+ $link = $el.parent().parent().find( 'a:not(.icon)' );
 295+
 296+ // Grab category text
 297+ input = $el.parent().find( '.mw-addcategory-input' ).val();
 298+
 299+ // Split categoryname and sortkey
 300+ var arr = input.split( '|', 2 );
 301+ category = arr[0];
 302+ sortkey = arr[1]; // Is usually undefined, ie. if there was no '|' in the input.
 303+
 304+ // Grab text
 305+ var isAdded = $link.hasClass( 'mw-added-category' );
 306+ ajaxcat.resetCatLink( $link );
 307+ categoryOld = $link.text();
 308+
 309+ // If something changed and the new cat is already on the page, delete it.
 310+ if ( categoryOld !== category && ajaxcat.containsCat( category ) ) {
 311+ $link.data( 'deleteButton' ).click();
 312+ return;
 313+ }
 314+
 315+ // Resolve redirects
 316+ ajaxcat.resolveRedirects( category, function( resolvedCatTitle ) {
 317+ ajaxcat.handleCategoryEdit( $link, categoryOld, resolvedCatTitle, sortkey, isAdded );
 318+ });
 319+ };
 320+
 321+ /**
 322+ * Handle delete category submit. Not to be called directly.
 323+ *
 324+ * @context Element
 325+ * @param e {jQuery Event}
 326+ */
 327+ this.handleDeleteLink = function( e ) {
 328+ var $el = $( this ),
 329+ $link = $el.parent().find( 'a:not(.icon)' ),
 330+ category = $link.text();
 331+
 332+ if ( $link.is( '.mw-added-category, .mw-changed-category' ) ) {
 333+ // We're just cancelling the addition or edit
 334+ ajaxcat.resetCatLink( $link, $link.hasClass( 'mw-added-category' ) );
 335+ return;
 336+ } else if ( $link.is( '.mw-removed-category' ) ) {
 337+ // It's already removed...
 338+ return;
 339+ }
 340+ ajaxcat.handleCategoryDelete( $link, category );
 341+ };
 342+
 343+ /**
 344+ * When multiEdit mode is enabled,
 345+ * this is called when the user clicks "save all"
 346+ * Combines the dialogDescriptions and edit functions.
 347+ *
 348+ * @context Element
 349+ * @return ?
 350+ */
 351+ this.handleStashedCategories = function() {
 352+
 353+ // Remove "holes" in array
 354+ var dialogDescriptions = $.grep( ajaxcat.stash.dialogDescriptions, function( n, i ) {
 355+ return n;
 356+ } );
 357+
 358+ if ( dialogDescriptions.length < 1 ) {
 359+ // Nothing to do here.
 360+ ajaxcat.saveAllButton.hide();
 361+ ajaxcat.cancelAllButton.hide();
 362+ return;
 363+ } else {
 364+ dialogDescriptions = dialogDescriptions.join( '<br/>' );
 365+ }
 366+
 367+ // Remove "holes" in array
 368+ var summaryShort = $.grep( ajaxcat.stash.editSummaries, function( n,i ) {
 369+ return n;
 370+ } );
 371+ summaryShort = summaryShort.join( ', ' );
 372+
 373+ var fns = ajaxcat.stash.fns;
 374+
 375+ ajaxcat.doConfirmEdit( {
 376+ modFn: function( oldtext ) {
 377+ // Run the text through all action functions
 378+ var newtext = oldtext;
 379+ for ( var i = 0; i < fns.length; i++ ) {
 380+ if ( $.isFunction( fns[i] ) ) {
 381+ newtext = fns[i]( newtext );
 382+ if ( newtext === false ) {
 383+ return false;
 384+ }
 385+ }
 386+ }
 387+ return newtext;
 388+ },
 389+ dialogDescription: dialogDescriptions,
 390+ editSummary: summaryShort,
 391+ doneFn: function() {
 392+ ajaxcat.resetAll( true );
 393+ },
 394+ $link: null,
 395+ action: 'all'
 396+ } );
 397+ };
 398+};
 399+
 400+/* Public methods */
 401+
 402+mw.InlineCategorizer.prototype = {
 403+ /**
 404+ * Create the UI
 405+ */
 406+ setup: function() {
 407+ // Only do it for articles.
 408+ if ( !mw.config.get( 'wgIsArticle' ) ) {
 409+ return;
 410+ }
 411+ var options = this.options,
 412+ ajaxcat = this,
 413+ // Create [Add Category] link
 414+ $addLink = createButton( 'icon-add',
 415+ mw.msg( 'inlinecategorizer-add-category' ),
 416+ 'mw-ajax-addcategory',
 417+ mw.msg( 'inlinecategorizer-add-category' )
 418+ ).click( function() {
 419+ $( this ).nextAll().toggle().filter( '.mw-addcategory-input' ).focus();
 420+ });
 421+
 422+ // Create add category prompt
 423+ this.addContainer = this.makeSuggestionBox( '', this.handleAddLink, mw.msg( 'inlinecategorizer-add-category-submit' ) );
 424+ this.addContainer.children().hide();
 425+ this.addContainer.prepend( $addLink );
 426+
 427+ // Create edit & delete link for each category.
 428+ $( '#catlinks' ).find( 'li a' ).each( function() {
 429+ ajaxcat.createCatButtons( $( this ) );
 430+ });
 431+
 432+ options.$containerNormal.append( this.addContainer );
 433+
 434+ // @todo Make more clickable
 435+ this.saveAllButton = createButton( 'icon-tick',
 436+ mw.msg( 'inlinecategorizer-confirm-save-all' ),
 437+ '',
 438+ mw.msg( 'inlinecategorizer-confirm-save-all' )
 439+ );
 440+ this.cancelAllButton = createButton( 'icon-close',
 441+ mw.msg( 'inlinecategorizer-cancel-all' ),
 442+ '',
 443+ mw.msg( 'inlinecategorizer-cancel-all' )
 444+ );
 445+ this.saveAllButton.click( this.handleStashedCategories ).hide();
 446+ this.cancelAllButton.click( function() {
 447+ ajaxcat.resetAll( false );
 448+ } ).hide();
 449+ options.$containerNormal.append( this.saveAllButton ).append( this.cancelAllButton );
 450+ options.$container.append( this.addContainer );
 451+ },
 452+
 453+ /**
 454+ * Insert a newly added category into the DOM.
 455+ *
 456+ * @param catTitle {mw.Title} Category title for which a link should be created.
 457+ * @return {jQuery}
 458+ */
 459+ createCatLink: function( catTitle ) {
 460+ var catName = catTitle.getMainText(),
 461+ $catLinkWrapper = $( this.options.catLinkWrapper ),
 462+ $anchor = $( '<a>' )
 463+ .text( catName )
 464+ .attr( {
 465+ target: '_blank',
 466+ href: catTitle.getUrl()
 467+ } );
 468+
 469+ $catLinkWrapper.append( $anchor );
 470+
 471+ this.createCatButtons( $anchor );
 472+
 473+ return $anchor;
 474+ },
 475+
 476+ /**
 477+ * Create a suggestion box for use in edit/add dialogs
 478+ * @param prefill {String} Prefill input
 479+ * @param callback {Function} Called on submit
 480+ * @param buttonVal {String} Button text
 481+ */
 482+ makeSuggestionBox: function( prefill, callback, buttonVal ) {
 483+ // Create add category prompt
 484+ var $promptContainer = $( '<div class="mw-addcategory-prompt"></div>' ),
 485+ $promptTextbox = $( '<input type="text" size="30" class="mw-addcategory-input"></input>' ),
 486+ $addButton = $( '<input type="button" class="mw-addcategory-button"></input>' ),
 487+ ajaxcat = this;
 488+
 489+ if ( prefill !== '' ) {
 490+ $promptTextbox.val( prefill );
 491+ }
 492+
 493+ $addButton
 494+ .val( buttonVal )
 495+ .click( callback );
 496+
 497+ $promptTextbox
 498+ .keyup( function( e ) {
 499+ if ( e.keyCode === 13 ) {
 500+ $addButton.click();
 501+ }
 502+ } )
 503+ .suggestions( {
 504+ fetch: fetchSuggestions,
 505+ cancel: function() {
 506+ var req = this.data( 'suggestions-request' );
 507+ // XMLHttpRequest.abort is unimplemented in IE6, also returns nonstandard value of 'unknown' for typeof
 508+ if ( req && typeof req.abort !== 'unknown' && typeof req.abort !== 'undefined' && req.abort ) {
 509+ req.abort();
 510+ }
 511+ }
 512+ } )
 513+ .suggestions();
 514+
 515+ $promptContainer
 516+ .append( $promptTextbox )
 517+ .append( $addButton );
 518+
 519+ return $promptContainer;
 520+ },
 521+
 522+ /**
 523+ * Execute or queue a category addition.
 524+ *
 525+ * @param $link {jQuery} Anchor tag of category link inside #catlinks.
 526+ * @param catTitle {mw.Title} Instance of mw.Title of the category to be added.
 527+ * @param catSortkey {String} sort key (optional)
 528+ * @param noAppend
 529+ * @return {mw.inlineCategorizer}
 530+ */
 531+ handleCategoryAdd: function( $link, catTitle, catSortkey, noAppend ) {
 532+ var ajaxcat = this,
 533+ // Suffix is wikitext between '[[Category:Foo' and ']]'.
 534+ suffix = catSortkey ? '|' + catSortkey : '',
 535+ catName = catTitle.getMainText(),
 536+ catFull = catTitle.toText();
 537+
 538+ if ( this.containsCat( catName ) ) {
 539+ this.showError( mw.msg( 'inlinecategorizer-category-already-present', catName ) );
 540+ return this;
 541+ }
 542+
 543+ if ( !$link.length ) {
 544+ $link = this.createCatLink( catTitle );
 545+ }
 546+
 547+ // Mark red if missing
 548+ $link[(catTitle.exists() === false ? 'addClass' : 'removeClass')]( 'new' );
 549+
 550+ this.doConfirmEdit( {
 551+ modFn: function( oldText ) {
 552+ var newText = ajaxcat.runHooks( oldText, 'beforeAdd', catName );
 553+ newText = newText + "\n[[" + catFull + suffix + "]]\n";
 554+ return ajaxcat.runHooks( newText, 'afterAdd', catName );
 555+ },
 556+ dialogDescription: mw.message( 'inlinecategorizer-add-category-summary', catName ).escaped(),
 557+ editSummary: '+[[' + catFull + ']]',
 558+ doneFn: function( unsaved ) {
 559+ if ( !noAppend ) {
 560+ ajaxcat.options.$container
 561+ .find( '#mw-normal-catlinks > .mw-addcategory-prompt' ).children( 'input' ).hide();
 562+ ajaxcat.options.$container
 563+ .find( '#mw-normal-catlinks ul' ).append( $link.parent() );
 564+ } else {
 565+ // Remove input box & button
 566+ $link.data( 'deleteButton' ).click();
 567+
 568+ // Update link text and href
 569+ $link.show().text( catName ).attr( 'href', catTitle.getUrl() );
 570+ }
 571+ if ( unsaved ) {
 572+ $link.addClass( 'mw-added-category' );
 573+ }
 574+ $( '.mw-ajax-addcategory' ).click();
 575+ },
 576+ $link: $link,
 577+ action: 'add'
 578+ } );
 579+ return this;
 580+ },
 581+
 582+ /**
 583+ * Execute or queue a category edit.
 584+ *
 585+ * @param $link {jQuery} Anchor tag of category link in #catlinks.
 586+ * @param oldCatName {String} Name of category before edit
 587+ * @param catTitle {mw.Title} Instance of mw.Title for new category
 588+ * @param catSortkey {String} Sort key of new category link (optional)
 589+ * @param isAdded {Boolean} True if this is a new link, false if it changed an existing one
 590+ */
 591+ handleCategoryEdit: function( $link, oldCatName, catTitle, catSortkey, isAdded ) {
 592+ var ajaxcat = this,
 593+ catName = catTitle.getMainText();
 594+
 595+ // Category add needs to be handled differently
 596+ if ( isAdded ) {
 597+ // Pass sortkey back
 598+ this.handleCategoryAdd( $link, catTitle, catSortkey, true );
 599+ return;
 600+ }
 601+
 602+ // User didn't change anything, trigger delete
 603+ // @todo Document why it's deleted.
 604+ if ( oldCatName === catName ) {
 605+ $link.data( 'deleteButton' ).click();
 606+ return;
 607+ }
 608+
 609+ // Mark red if missing
 610+ $link[(catTitle.exists() === false ? 'addClass' : 'removeClass')]( 'new' );
 611+
 612+ var categoryRegex = buildRegex( oldCatName ),
 613+ editSummary = '[[' + new mw.Title( oldCatName, catNsId ).toText() + ']] -> [[' + catTitle.toText() + ']]';
 614+
 615+ ajaxcat.doConfirmEdit({
 616+ modFn: function( oldText ) {
 617+ var newText = ajaxcat.runHooks( oldText, 'beforeChange', oldCatName, catName ),
 618+ matches = newText.match( categoryRegex );
 619+
 620+ // Old cat wasn't found, likely to be transcluded
 621+ if ( !$.isArray( matches ) ) {
 622+ ajaxcat.showError( mw.msg( 'inlinecategorizer-edit-category-error' ) );
 623+ return false;
 624+ }
 625+
 626+ var suffix = catSortkey ? '|' + catSortkey : matches[0].replace( categoryRegex, '$2' ),
 627+ newCategoryWikitext = '[[' + catTitle + suffix + ']]';
 628+
 629+ if ( matches.length > 1 ) {
 630+ // The category is duplicated. Remove all but one match
 631+ for ( var i = 1; i < matches.length; i++ ) {
 632+ oldText = oldText.replace( matches[i], '' );
 633+ }
 634+ }
 635+ newText = oldText.replace( categoryRegex, newCategoryWikitext );
 636+
 637+ return ajaxcat.runHooks( newText, 'afterChange', oldCatName, catName );
 638+ },
 639+ dialogDescription: mw.message( 'inlinecategorizer-edit-category-summary', oldCatName, catName ).escaped(),
 640+ editSummary: editSummary,
 641+ doneFn: function( unsaved ) {
 642+ // Remove input box & button
 643+ $link.data( 'deleteButton' ).click();
 644+
 645+ // Update link text and href
 646+ $link.show().text( catName ).attr( 'href', catTitle.getUrl() );
 647+ if ( unsaved ) {
 648+ $link.data( 'origCat', oldCatName ).addClass( 'mw-changed-category' );
 649+ }
 650+ },
 651+ $link: $link,
 652+ action: 'edit'
 653+ });
 654+ },
 655+
 656+ /**
 657+ * Checks the API whether the category in question is a redirect.
 658+ * Also returns existance info (to color link red/blue)
 659+ * @param category {String} Name of category to resolve
 660+ * @param callback {Function} Called with 1 argument (mw.Title object)
 661+ */
 662+ resolveRedirects: function( category, callback ) {
 663+ if ( !this.options.resolveRedirects ) {
 664+ callback( category, true );
 665+ return;
 666+ }
 667+ var catTitle = new mw.Title( category, catNsId ),
 668+ queryVars = {
 669+ action:'query',
 670+ titles: catTitle.toString(),
 671+ redirects: 1,
 672+ format: 'json'
 673+ };
 674+
 675+ $.getJSON( mw.util.wikiScript( 'api' ), queryVars, function( json ) {
 676+ var redirect = json.query.redirects,
 677+ exists = !json.query.pages[-1];
 678+
 679+ // If it's a redirect 'exists' is for the target, not the origin
 680+ if ( redirect ) {
 681+ // Register existance of redirect origin as well,
 682+ // a non-existent page can't be a redirect.
 683+ mw.Title.exist.set( catTitle.toString(), true );
 684+
 685+ // Override title with the redirect target
 686+ catTitle = new mw.Title( redirect[0].to ).getMainText();
 687+ }
 688+
 689+ // Register existence
 690+ mw.Title.exist.set( catTitle.toString(), exists );
 691+
 692+ callback( catTitle );
 693+ } );
 694+ },
 695+
 696+ /**
 697+ * Append edit and remove buttons to a given category link
 698+ *
 699+ * @param DOMElement element Anchor element, to which the buttons should be appended.
 700+ * @return {mw.inlineCategorizer}
 701+ */
 702+ createCatButtons: function( $element ) {
 703+ var deleteButton = createButton( 'icon-close', mw.msg( 'inlinecategorizer-remove-category' ) ),
 704+ editButton = createButton( 'icon-edit', mw.msg( 'inlinecategorizer-edit-category' ) ),
 705+ saveButton = createButton( 'icon-tick', mw.msg( 'inlinecategorizer-confirm-save' ) ).hide(),
 706+ ajaxcat = this;
 707+
 708+ deleteButton.click( this.handleDeleteLink );
 709+ editButton.click( ajaxcat.createEditInterface );
 710+
 711+ $element.after( deleteButton ).after( editButton );
 712+
 713+ // Save references to all links and buttons
 714+ $element.data( {
 715+ deleteButton: deleteButton,
 716+ editButton: editButton,
 717+ saveButton: saveButton
 718+ } );
 719+ editButton.data( {
 720+ link: $element
 721+ } );
 722+ return this;
 723+ },
 724+
 725+ /**
 726+ * Append spinner wheel to element.
 727+ * @param $el {jQuery}
 728+ * @return {mw.inlineCategorizer}
 729+ */
 730+ addProgressIndicator: function( $el ) {
 731+ $el.append( $( '<div>' ).addClass( 'mw-ajax-loader' ) );
 732+ return this;
 733+ },
 734+
 735+ /**
 736+ * Find and remove spinner wheel from inside element.
 737+ * @param $el {jQuery}
 738+ * @return {mw.inlineCategorizer}
 739+ */
 740+ removeProgressIndicator: function( $el ) {
 741+ $el.find( '.mw-ajax-loader' ).remove();
 742+ return this;
 743+ },
 744+
 745+ /**
 746+ * Parse the DOM $container and build a list of
 747+ * present categories.
 748+ *
 749+ * @return {Array} All categories.
 750+ */
 751+ getCats: function() {
 752+ var cats = this.options.$container
 753+ .find( this.options.categoryLinkSelector )
 754+ .map( function() {
 755+ return $.trim( $( this ).text() );
 756+ } );
 757+ return cats;
 758+ },
 759+
 760+ /**
 761+ * Check whether a passed category is present in the DOM.
 762+ *
 763+ * @param newCat {String} Category name to be checked for.
 764+ * @return {Boolean}
 765+ */
 766+ containsCat: function( newCat ) {
 767+ newCat = $.ucFirst( newCat );
 768+ var match = false;
 769+ $.each( this.getCats(), function(i, cat) {
 770+ if ( $.ucFirst( cat ) === newCat ) {
 771+ match = true;
 772+ // Stop once we have a match
 773+ return false;
 774+ }
 775+ } );
 776+ return match;
 777+ },
 778+
 779+ /**
 780+ * Execute or queue a category delete.
 781+ *
 782+ * @param $link {jQuery}
 783+ * @param category
 784+ * @return ?
 785+ */
 786+ handleCategoryDelete: function( $link, category ) {
 787+ var categoryRegex = buildRegex( category, true ),
 788+ ajaxcat = this;
 789+
 790+ this.doConfirmEdit({
 791+ modFn: function( oldText ) {
 792+ var newText = ajaxcat.runHooks( oldText, 'beforeDelete', category );
 793+ newText = newText.replace( categoryRegex, '' );
 794+
 795+ if ( newText === oldText ) {
 796+ ajaxcat.showError( mw.msg( 'inlinecategorizer-remove-category-error' ) );
 797+ return false;
 798+ }
 799+
 800+ return ajaxcat.runHooks( newText, 'afterDelete', category );
 801+ },
 802+ dialogDescription: mw.message( 'inlinecategorizer-remove-category-summary', category ).escaped(),
 803+ editSummary: '-[[' + new mw.Title( category, catNsId ) + ']]',
 804+ doneFn: function( unsaved ) {
 805+ if ( unsaved ) {
 806+ $link.addClass( 'mw-removed-category' );
 807+ } else {
 808+ $link.parent().remove();
 809+ }
 810+ },
 811+ $link: $link,
 812+ action: 'delete'
 813+ });
 814+ },
 815+
 816+ /**
 817+ * Takes a category link element
 818+ * and strips all data from it.
 819+ *
 820+ * @param $link {jQuery}
 821+ * @param del {Boolean}
 822+ * @param dontRestoreText {Boolean}
 823+ * @return ?
 824+ */
 825+ resetCatLink: function( $link, del, dontRestoreText ) {
 826+ $link.removeClass( 'mw-removed-category mw-added-category mw-changed-category' );
 827+ var data = $link.data();
 828+
 829+ if ( typeof data.stashIndex === 'number' ) {
 830+ this.removeStashItem( data.stashIndex );
 831+ }
 832+ if ( del ) {
 833+ $link.parent().remove();
 834+ return;
 835+ }
 836+ if ( data.origCat && !dontRestoreText ) {
 837+ var catTitle = new mw.Title( data.origCat, catNsId );
 838+ $link.text( catTitle.getMainText() );
 839+ $link.attr( 'href', catTitle.getUrl() );
 840+ }
 841+
 842+ $link.removeData();
 843+
 844+ // Re-add data
 845+ $link.data( {
 846+ saveButton: data.saveButton,
 847+ deleteButton: data.deleteButton,
 848+ editButton: data.editButton
 849+ } );
 850+ },
 851+
 852+ /**
 853+ * Do the actual edit.
 854+ * Gets token & text from api, runs it through fn
 855+ * and saves it with summary.
 856+ * @param page {String} Pagename
 857+ * @param fn {Function} edit function
 858+ * @param summary {String}
 859+ * @param doneFn {String} Callback after all is done
 860+ */
 861+ doEdit: function( page, fn, summary, doneFn ) {
 862+ // Get an edit token for the page.
 863+ var getTokenVars = {
 864+ action: 'query',
 865+ prop: 'info|revisions',
 866+ intoken: 'edit',
 867+ titles: page,
 868+ rvprop: 'content|timestamp',
 869+ format: 'json'
 870+ }, ajaxcat = this;
 871+
 872+ $.post(
 873+ mw.util.wikiScript( 'api' ),
 874+ getTokenVars,
 875+ function( json ) {
 876+ if ( 'error' in json ) {
 877+ ajaxcat.showError( mw.msg( 'inlinecategorizer-api-error', json.error.code, json.error.info ) );
 878+ return;
 879+ } else if ( json.query && json.query.pages ) {
 880+ var infos = json.query.pages;
 881+ } else {
 882+ ajaxcat.showError( mw.msg( 'inlinecategorizer-api-unknown-error' ) );
 883+ return;
 884+ }
 885+
 886+ $.each( infos, function( pageid, data ) {
 887+ var token = data.edittoken,
 888+ timestamp = data.revisions[0].timestamp,
 889+ oldText = data.revisions[0]['*'],
 890+ nowikiKey = generateRandomId(), // Unique ID for nowiki replacement
 891+ nowikiFragments = []; // Nowiki fragments will be stored here during the changes
 892+
 893+ // Replace all nowiki parts with unique keys..
 894+ oldText = replaceNowikis( oldText, nowikiKey, nowikiFragments );
 895+
 896+ // ..then apply the changes to the page text..
 897+ var newText = fn( oldText );
 898+ if ( newText === false ) {
 899+ return;
 900+ }
 901+
 902+ // ..and restore the nowiki parts back.
 903+ newText = restoreNowikis( newText, nowikiKey, nowikiFragments );
 904+
 905+ var postEditVars = {
 906+ action: 'edit',
 907+ title: page,
 908+ text: newText,
 909+ summary: summary,
 910+ token: token,
 911+ basetimestamp: timestamp,
 912+ format: 'json'
 913+ };
 914+
 915+ $.post(
 916+ mw.util.wikiScript( 'api' ),
 917+ postEditVars,
 918+ doneFn,
 919+ 'json'
 920+ )
 921+ .error( function( xhr, text, error ) {
 922+ ajaxcat.showError( mw.msg( 'inlinecategorizer-api-error', text, error ) );
 923+ });
 924+ } );
 925+ },
 926+ 'json'
 927+ ).error( function( xhr, text, error ) {
 928+ ajaxcat.showError( mw.msg( 'inlinecategorizer-api-error', text, error ) );
 929+ } );
 930+ },
 931+
 932+ /**
 933+ * This gets called by all action buttons
 934+ * Displays a dialog to confirm the action
 935+ * Afterwards do the actual edit.
 936+ *
 937+ * @param props {Object}:
 938+ * - modFn {Function} text-modifying function
 939+ * - dialogDescription {String} Changes done (HTML for in the dialog, escape before hand if needed)
 940+ * - editSummary {String} Changes done (text for the edit summary)
 941+ * - doneFn {Function} callback after everything is done
 942+ * - $link {jQuery}
 943+ * - action
 944+ * @return {mw.inlineCategorizer}
 945+ */
 946+ doConfirmEdit: function( props ) {
 947+ var summaryHolder, reasonBox, dialog, submitFunction,
 948+ buttons = {},
 949+ dialogOptions = {
 950+ AutoOpen: true,
 951+ buttons: buttons,
 952+ width: 450
 953+ },
 954+ ajaxcat = this;
 955+
 956+ // Check whether to use multiEdit mode:
 957+ if ( this.options.multiEdit && props.action !== 'all' ) {
 958+
 959+ // Stash away
 960+ props.$link
 961+ .data( 'stashIndex', this.stash.fns.length )
 962+ .data( 'summary', props.dialogDescription );
 963+
 964+ this.stash.dialogDescriptions.push( props.dialogDescription );
 965+ this.stash.editSummaries.push( props.editSummary );
 966+ this.stash.fns.push( props.modFn );
 967+
 968+ this.saveAllButton.show();
 969+ this.cancelAllButton.show();
 970+
 971+ // Clear input field after action
 972+ ajaxcat.addContainer.find( '.mw-addcategory-input' ).val( '' );
 973+
 974+ // This only does visual changes, fire done and return.
 975+ props.doneFn( true );
 976+ return this;
 977+ }
 978+
 979+ // Summary of the action to be taken
 980+ summaryHolder = $( '<p>' )
 981+ .html( '<strong>' + mw.message( 'inlinecategorizer-category-question' ).escaped() + '</strong><br/>' + props.dialogDescription );
 982+
 983+ // Reason textbox.
 984+ reasonBox = $( '<input type="text" size="45"></input>' )
 985+ .addClass( 'mw-ajax-confirm-reason' );
 986+
 987+ // Produce a confirmation dialog
 988+ dialog = $( '<div>' )
 989+ .addClass( 'mw-ajax-confirm-dialog' )
 990+ .attr( 'title', mw.msg( 'inlinecategorizer-confirm-title' ) )
 991+ .append( summaryHolder )
 992+ .append( reasonBox );
 993+
 994+ // Submit button
 995+ submitFunction = function() {
 996+ ajaxcat.addProgressIndicator( dialog );
 997+ ajaxcat.doEdit(
 998+ mw.config.get( 'wgPageName' ),
 999+ props.modFn,
 1000+ props.editSummary + ': ' + reasonBox.val(),
 1001+ function() {
 1002+ props.doneFn();
 1003+
 1004+ // Clear input field after successful edit
 1005+ ajaxcat.addContainer.find( '.mw-addcategory-input' ).val( '' );
 1006+
 1007+ dialog.dialog( 'close' );
 1008+ ajaxcat.removeProgressIndicator( dialog );
 1009+ }
 1010+ );
 1011+ };
 1012+
 1013+ buttons[mw.msg( 'inlinecategorizer-confirm-save' )] = submitFunction;
 1014+
 1015+ dialog.dialog( dialogOptions ).keyup( function( e ) {
 1016+ // Close on enter
 1017+ if ( e.keyCode === 13 ) {
 1018+ submitFunction();
 1019+ }
 1020+ } );
 1021+
 1022+ return this;
 1023+ },
 1024+
 1025+ /**
 1026+ * @param index {Number|jQuery} Stash index or jQuery object of stash item.
 1027+ * @return {mw.inlineCategorizer}
 1028+ */
 1029+ removeStashItem: function( i ) {
 1030+ if ( typeof i !== 'number' ) {
 1031+ i = i.data( 'stashIndex' );
 1032+ }
 1033+
 1034+ try {
 1035+ delete this.stash.fns[i];
 1036+ delete this.stash.dialogDescriptions[i];
 1037+ } catch(e) {}
 1038+
 1039+ if ( $.isEmpty( this.stash.fns ) ) {
 1040+ this.stash.fns = [];
 1041+ this.stash.dialogDescriptions = [];
 1042+ this.stash.editSummaries = [];
 1043+ this.saveAllButton.hide();
 1044+ this.cancelAllButton.hide();
 1045+ }
 1046+ return this;
 1047+ },
 1048+
 1049+ /**
 1050+ * Reset all data from the category links and the stash.
 1051+ *
 1052+ * @param del {Boolean} Delete any category links with .mw-removed-category
 1053+ * @return {mw.inlineCategorizer}
 1054+ */
 1055+ resetAll: function( del ) {
 1056+ var $links = this.options.$container.find( this.options.categoryLinkSelector ),
 1057+ $del = $([]),
 1058+ ajaxcat = this;
 1059+
 1060+ if ( del ) {
 1061+ $del = $links.filter( '.mw-removed-category' ).parent();
 1062+ }
 1063+
 1064+ $links.each( function() {
 1065+ ajaxcat.resetCatLink( $( this ), false, del );
 1066+ } );
 1067+
 1068+ $del.remove();
 1069+
 1070+ this.options.$container.find( '#mw-hidden-catlinks' ).remove();
 1071+
 1072+ return this;
 1073+ },
 1074+
 1075+ /**
 1076+ * Add hooks
 1077+ * Currently available: beforeAdd, beforeChange, beforeDelete,
 1078+ * afterAdd, afterChange, afterDelete
 1079+ * If the hook function returns false, all changes are aborted.
 1080+ *
 1081+ * @param string type Type of hook to add
 1082+ * @param function fn Hook function. The following vars are passed to it:
 1083+ * 1. oldtext: The wikitext before the hook
 1084+ * 2. category: The deleted, added, or changed category
 1085+ * 3. (only for beforeChange/afterChange): newcategory
 1086+ */
 1087+ addHook: function( type, fn ) {
 1088+ if ( !this.hooks[type] || !$.isFunction( fn ) ) {
 1089+ return;
 1090+ }
 1091+ else {
 1092+ this.hooks[type].push( fn );
 1093+ }
 1094+ },
 1095+
 1096+
 1097+ /**
 1098+ * Open a dismissable error dialog
 1099+ *
 1100+ * @param string str The error description
 1101+ */
 1102+ showError: function( str ) {
 1103+ var oldDialog = $( '.mw-ajax-confirm-dialog' ),
 1104+ buttons = {},
 1105+ dialogOptions = {
 1106+ buttons: buttons,
 1107+ AutoOpen: true,
 1108+ title: mw.msg( 'inlinecategorizer-error-title' )
 1109+ };
 1110+
 1111+ this.removeProgressIndicator( oldDialog );
 1112+ oldDialog.dialog( 'close' );
 1113+
 1114+ var dialog = $( '<div>' ).text( str );
 1115+
 1116+ mw.util.$content.append( dialog );
 1117+
 1118+ buttons[mw.msg( 'inlinecategorizer-confirm-ok' )] = function( e ) {
 1119+ dialog.dialog( 'close' );
 1120+ };
 1121+
 1122+ dialog.dialog( dialogOptions ).keyup( function( e ) {
 1123+ if ( e.keyCode === 13 ) {
 1124+ dialog.dialog( 'close' );
 1125+ }
 1126+ } );
 1127+ },
 1128+
 1129+ /**
 1130+ * @param oldtext
 1131+ * @param type
 1132+ * @param category
 1133+ * @param categoryNew
 1134+ * @return oldtext
 1135+ */
 1136+ runHooks: function( oldtext, type, category, categoryNew ) {
 1137+ // No hooks registered
 1138+ if ( !this.hooks[type] ) {
 1139+ return oldtext;
 1140+ } else {
 1141+ for ( var i = 0; i < this.hooks[type].length; i++ ) {
 1142+ oldtext = this.hooks[type][i]( oldtext, category, categoryNew );
 1143+ if ( oldtext === false ) {
 1144+ this.showError( mw.msg( 'inlinecategorizer-category-hook-error', category ) );
 1145+ return;
 1146+ }
 1147+ }
 1148+ return oldtext;
 1149+ }
 1150+ }
 1151+};
 1152+
 1153+} )( jQuery );
Index: trunk/extensions/InlineCategorizer/modules/ext.inlineCategorizer.core.js
@@ -0,0 +1,1152 @@
 2+/**
 3+ * The core of InlineCategorizer
 4+ *
 5+ * @author Michael Dale, 2009
 6+ * @author Leo Koppelkamm, 2011
 7+ * @author Timo Tijhof, 2011
 8+ *
 9+ * Relies on: mw.config (wgFormattedNamespaces, wgNamespaceIds,
 10+ * wgCaseSensitiveNamespaces, wgUserGroups), mw.util.wikiGetlink
 11+ */
 12+( function( $ ) {
 13+
 14+ /* Local scope */
 15+
 16+ var catNsId = mw.config.get( 'wgNamespaceIds' ).category,
 17+ defaultOptions = {
 18+ catLinkWrapper: '<li>',
 19+ $container: $( '.catlinks' ),
 20+ $containerNormal: $( '#mw-normal-catlinks' ),
 21+ categoryLinkSelector: 'li a:not(.icon)',
 22+ multiEdit: $.inArray( 'user', mw.config.get( 'wgUserGroups' ) ) !== -1,
 23+ resolveRedirects: true
 24+ },
 25+ isCatNsSensitive = $.inArray( 14, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1;
 26+
 27+ /**
 28+ * @return {String}
 29+ */
 30+ function clean( s ) {
 31+ if ( typeof s === 'string' ) {
 32+ return s.replace( /[\x00-\x1f\x23\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f\s]+/g, '' );
 33+ }
 34+ return '';
 35+ }
 36+
 37+ /**
 38+ * Generates a random id out of 62 alpha-numeric characters.
 39+ *
 40+ * @param {Number} Length of id (optional, defaults to 32)
 41+ * @return {String}
 42+ */
 43+ function generateRandomId( idLength ) {
 44+ idLength = typeof idLength === 'number' ? idLength : 32;
 45+ var seed = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
 46+ id = '';
 47+ for ( var r, i = 0; i < idLength; i++ ) {
 48+ r = Math.floor( Math.random() * seed.length );
 49+ id += seed.substring( r, r + 1 );
 50+ }
 51+ return id;
 52+ }
 53+
 54+ /**
 55+ * Helper function for $.fn.suggestions
 56+ *
 57+ * @context {jQuery}
 58+ * @param value {String} Textbox value.
 59+ */
 60+ function fetchSuggestions( value ) {
 61+ var request,
 62+ $el = this,
 63+ catName = clean( value );
 64+
 65+ request = $.ajax( {
 66+ url: mw.util.wikiScript( 'api' ),
 67+ data: {
 68+ action: 'query',
 69+ list: 'allpages',
 70+ apnamespace: catNsId,
 71+ apprefix: catName,
 72+ format: 'json'
 73+ },
 74+ dataType: 'json',
 75+ success: function( data ) {
 76+ // Process data.query.allpages into an array of titles
 77+ var pages = data.query.allpages,
 78+ titleArr = $.map( pages, function( page ) {
 79+ return new mw.Title( page.title ).getMainText();
 80+ } );
 81+
 82+ $el.suggestions( 'suggestions', titleArr );
 83+ }
 84+ } );
 85+ $el.data( 'suggestions-request', request );
 86+ }
 87+
 88+ /**
 89+ * Replace <nowiki> and comments with unique keys in the page text.
 90+ *
 91+ * @param text {String}
 92+ * @param id {String} Unique key for this nowiki replacement layer call.
 93+ * @param keys {Array} Array where fragments will be stored in.
 94+ * @return {String}
 95+ */
 96+ function replaceNowikis( text, id, keys ) {
 97+ var matches = text.match( /(<nowiki\>[\s\S]*?<\/nowiki>|<\!--[\s\S]*?--\>)/g );
 98+ for ( var i = 0; matches && i < matches.length; i++ ) {
 99+ keys[i] = matches[i];
 100+ text = text.replace( matches[i], '' + id + '-' + i );
 101+ }
 102+ return text;
 103+ }
 104+
 105+ /**
 106+ * Restore <nowiki> and comments from unique keys in the page text.
 107+ *
 108+ * @param text {String}
 109+ * @param id {String} Unique key of the layer to be restored, as passed to replaceNowikis().
 110+ * @param keys {Array} Array where fragements should be fetched from.
 111+ * @return {String}
 112+ */
 113+ function restoreNowikis( text, id, keys ) {
 114+ for ( var i = 0; i < keys.length; i++ ) {
 115+ text = text.replace( '' + id + '-' + i, keys[i] );
 116+ }
 117+ return text;
 118+ }
 119+
 120+ /**
 121+ * Makes regex string caseinsensitive.
 122+ * Useful when 'i' flag can't be used.
 123+ * Return stuff like [Ff][Oo][Oo]
 124+ *
 125+ * @param string {String} Regex string
 126+ * @return {String} Processed regex string
 127+ */
 128+ function makeCaseInsensitive( string ) {
 129+ var newString = '';
 130+ for ( var i = 0; i < string.length; i++ ) {
 131+ newString += '[' + string.charAt( i ).toUpperCase() + string.charAt( i ).toLowerCase() + ']';
 132+ }
 133+ return newString;
 134+ }
 135+
 136+ /**
 137+ * Build a regex that matches legal invocations of the passed category.
 138+ * @param category {String}
 139+ * @param matchLineBreak {Boolean} Match one following linebreak as well?
 140+ * @return {RegExp}
 141+ */
 142+ function buildRegex( category, matchLineBreak ) {
 143+ var categoryRegex, categoryNSFragment,
 144+ titleFragment = $.escapeRE( category ).replace( /( |_)/g, '[ _]' ),
 145+ firstChar = titleFragment.charAt( 0 );
 146+
 147+ // Filter out all names for category namespace
 148+ categoryNSFragment = $.map( mw.config.get( 'wgNamespaceIds' ), function( id, name ) {
 149+ if ( id === catNsId ) {
 150+ name = $.escapeRE( name );
 151+ return !isCatNsSensitive ? makeCaseInsensitive( name ) : name;
 152+ }
 153+ // Otherwise don't include in categoryNSFragment
 154+ return null;
 155+ } ).join( '|' );
 156+
 157+ firstChar = '[' + firstChar.toUpperCase() + firstChar.toLowerCase() + ']';
 158+ titleFragment = firstChar + titleFragment.substr( 1 );
 159+ categoryRegex = '\\[\\[(' + categoryNSFragment + ')' + '[ _]*' + ':' + '[ _]*' + titleFragment + '[ _]*' + '(\\|[^\\]]*)?\\]\\]';
 160+ if ( matchLineBreak ) {
 161+ categoryRegex += '[ \\t\\r]*\\n?';
 162+ }
 163+ return new RegExp( categoryRegex, 'g' );
 164+ }
 165+
 166+ /**
 167+ * Manufacture iconed button, with or without text.
 168+ *
 169+ * @param icon {String} The icon class.
 170+ * @param title {String} Title attribute.
 171+ * @param className {String} (optional) Additional classes to be added to the button.
 172+ * @param text {String} (optional) Text label of button.
 173+ * @return {jQuery} The button.
 174+ */
 175+ function createButton( icon, title, className, text ){
 176+ // We're adding a zero width space for IE7, it's got problems with empty nodes apparently
 177+ var $button = $( '<a>' )
 178+ .addClass( className || '' )
 179+ .attr( 'title', title )
 180+ .html( '&#8203;' );
 181+
 182+ if ( text ) {
 183+ var $icon = $( '<span>' ).addClass( 'icon ' + icon ).html( '&#8203;' );
 184+ $button.addClass( 'icon-parent' ).append( $icon ).append( mw.html.escape( text ) );
 185+ } else {
 186+ $button.addClass( 'icon ' + icon );
 187+ }
 188+ return $button;
 189+ }
 190+
 191+/**
 192+ * mw.InlineCategorizer
 193+ *
 194+ * @constructor
 195+ * @param options {Object}
 196+ */
 197+mw.InlineCategorizer = function( options ) {
 198+
 199+ this.options = options = $.extend( defaultOptions, options );
 200+
 201+ // Save scope in shortcut
 202+ var ajaxcat = this;
 203+
 204+ // Elements tied to this instance
 205+ this.saveAllButton = null;
 206+ this.cancelAllButton = null;
 207+ this.addContainer = null;
 208+
 209+ this.request = null;
 210+
 211+ // Stash and hooks
 212+ this.stash = {
 213+ dialogDescriptions: [],
 214+ editSummaries: [],
 215+ fns: []
 216+ };
 217+ this.hooks = {
 218+ beforeAdd: [],
 219+ beforeChange: [],
 220+ beforeDelete: [],
 221+ afterAdd: [],
 222+ afterChange: [],
 223+ afterDelete: []
 224+ };
 225+
 226+ /* Event handlers */
 227+
 228+ /**
 229+ * Handle add category submit. Not to be called directly.
 230+ *
 231+ * @context Element
 232+ * @param e {jQuery Event}
 233+ */
 234+ this.handleAddLink = function( e ) {
 235+ var $el = $( this ),
 236+ $link = $([]),
 237+ categoryText = $.ucFirst( $el.parent().find( '.mw-addcategory-input' ).val() || '' );
 238+
 239+ // Resolve redirects
 240+ ajaxcat.resolveRedirects( categoryText, function( resolvedCatTitle ) {
 241+ ajaxcat.handleCategoryAdd( $link, resolvedCatTitle, '', false );
 242+ } );
 243+ };
 244+
 245+ /**
 246+ * @context Element
 247+ * @param e {jQuery Event}
 248+ */
 249+ this.createEditInterface = function( e ) {
 250+ var $el = $( this ),
 251+ $link = $el.data( 'link' ),
 252+ category = $link.text(),
 253+ $input = ajaxcat.makeSuggestionBox( category,
 254+ ajaxcat.handleEditLink,
 255+ ajaxcat.options.multiEdit ? mw.msg( 'inlinecategorizer-confirm-ok' ) : mw.msg( 'inlinecategorizer-confirm-save' )
 256+ );
 257+
 258+ $link.after( $input ).hide();
 259+
 260+ $input.find( '.mw-addcategory-input' ).focus();
 261+
 262+ // Get the editButton associated with this category link,
 263+ // and hide it.
 264+ $link.data( 'editButton' ).hide();
 265+
 266+ // Get the deleteButton associated with this category link,
 267+ $link.data( 'deleteButton' )
 268+ // (re)set click handler
 269+ .unbind( 'click' )
 270+ .click( function() {
 271+ // When the delete button is clicked:
 272+ // - Remove the suggestion box
 273+ // - Show the link and it's edit button
 274+ // - (re)set the click handler again
 275+ $input.remove();
 276+ $link.show().data( 'editButton' ).show();
 277+ $( this )
 278+ .unbind( 'click' )
 279+ .click( ajaxcat.handleDeleteLink )
 280+ .attr( 'title', mw.msg( 'inlinecategorizer-remove-category' ) );
 281+ })
 282+ .attr( 'title', mw.msg( 'inlinecategorizer-cancel' ) );
 283+ };
 284+
 285+ /**
 286+ * Handle edit category submit. Not to be called directly.
 287+ *
 288+ * @context Element
 289+ * @param e {jQuery Event}
 290+ */
 291+ this.handleEditLink = function( e ) {
 292+ var input, category, sortkey, categoryOld,
 293+ $el = $( this ),
 294+ $link = $el.parent().parent().find( 'a:not(.icon)' );
 295+
 296+ // Grab category text
 297+ input = $el.parent().find( '.mw-addcategory-input' ).val();
 298+
 299+ // Split categoryname and sortkey
 300+ var arr = input.split( '|', 2 );
 301+ category = arr[0];
 302+ sortkey = arr[1]; // Is usually undefined, ie. if there was no '|' in the input.
 303+
 304+ // Grab text
 305+ var isAdded = $link.hasClass( 'mw-added-category' );
 306+ ajaxcat.resetCatLink( $link );
 307+ categoryOld = $link.text();
 308+
 309+ // If something changed and the new cat is already on the page, delete it.
 310+ if ( categoryOld !== category && ajaxcat.containsCat( category ) ) {
 311+ $link.data( 'deleteButton' ).click();
 312+ return;
 313+ }
 314+
 315+ // Resolve redirects
 316+ ajaxcat.resolveRedirects( category, function( resolvedCatTitle ) {
 317+ ajaxcat.handleCategoryEdit( $link, categoryOld, resolvedCatTitle, sortkey, isAdded );
 318+ });
 319+ };
 320+
 321+ /**
 322+ * Handle delete category submit. Not to be called directly.
 323+ *
 324+ * @context Element
 325+ * @param e {jQuery Event}
 326+ */
 327+ this.handleDeleteLink = function( e ) {
 328+ var $el = $( this ),
 329+ $link = $el.parent().find( 'a:not(.icon)' ),
 330+ category = $link.text();
 331+
 332+ if ( $link.is( '.mw-added-category, .mw-changed-category' ) ) {
 333+ // We're just cancelling the addition or edit
 334+ ajaxcat.resetCatLink( $link, $link.hasClass( 'mw-added-category' ) );
 335+ return;
 336+ } else if ( $link.is( '.mw-removed-category' ) ) {
 337+ // It's already removed...
 338+ return;
 339+ }
 340+ ajaxcat.handleCategoryDelete( $link, category );
 341+ };
 342+
 343+ /**
 344+ * When multiEdit mode is enabled,
 345+ * this is called when the user clicks "save all"
 346+ * Combines the dialogDescriptions and edit functions.
 347+ *
 348+ * @context Element
 349+ * @return ?
 350+ */
 351+ this.handleStashedCategories = function() {
 352+
 353+ // Remove "holes" in array
 354+ var dialogDescriptions = $.grep( ajaxcat.stash.dialogDescriptions, function( n, i ) {
 355+ return n;
 356+ } );
 357+
 358+ if ( dialogDescriptions.length < 1 ) {
 359+ // Nothing to do here.
 360+ ajaxcat.saveAllButton.hide();
 361+ ajaxcat.cancelAllButton.hide();
 362+ return;
 363+ } else {
 364+ dialogDescriptions = dialogDescriptions.join( '<br/>' );
 365+ }
 366+
 367+ // Remove "holes" in array
 368+ var summaryShort = $.grep( ajaxcat.stash.editSummaries, function( n,i ) {
 369+ return n;
 370+ } );
 371+ summaryShort = summaryShort.join( ', ' );
 372+
 373+ var fns = ajaxcat.stash.fns;
 374+
 375+ ajaxcat.doConfirmEdit( {
 376+ modFn: function( oldtext ) {
 377+ // Run the text through all action functions
 378+ var newtext = oldtext;
 379+ for ( var i = 0; i < fns.length; i++ ) {
 380+ if ( $.isFunction( fns[i] ) ) {
 381+ newtext = fns[i]( newtext );
 382+ if ( newtext === false ) {
 383+ return false;
 384+ }
 385+ }
 386+ }
 387+ return newtext;
 388+ },
 389+ dialogDescription: dialogDescriptions,
 390+ editSummary: summaryShort,
 391+ doneFn: function() {
 392+ ajaxcat.resetAll( true );
 393+ },
 394+ $link: null,
 395+ action: 'all'
 396+ } );
 397+ };
 398+};
 399+
 400+/* Public methods */
 401+
 402+mw.InlineCategorizer.prototype = {
 403+ /**
 404+ * Create the UI
 405+ */
 406+ setup: function() {
 407+ // Only do it for articles.
 408+ if ( !mw.config.get( 'wgIsArticle' ) ) {
 409+ return;
 410+ }
 411+ var options = this.options,
 412+ ajaxcat = this,
 413+ // Create [Add Category] link
 414+ $addLink = createButton( 'icon-add',
 415+ mw.msg( 'inlinecategorizer-add-category' ),
 416+ 'mw-ajax-addcategory',
 417+ mw.msg( 'inlinecategorizer-add-category' )
 418+ ).click( function() {
 419+ $( this ).nextAll().toggle().filter( '.mw-addcategory-input' ).focus();
 420+ });
 421+
 422+ // Create add category prompt
 423+ this.addContainer = this.makeSuggestionBox( '', this.handleAddLink, mw.msg( 'inlinecategorizer-add-category-submit' ) );
 424+ this.addContainer.children().hide();
 425+ this.addContainer.prepend( $addLink );
 426+
 427+ // Create edit & delete link for each category.
 428+ $( '#catlinks' ).find( 'li a' ).each( function() {
 429+ ajaxcat.createCatButtons( $( this ) );
 430+ });
 431+
 432+ options.$containerNormal.append( this.addContainer );
 433+
 434+ // @todo Make more clickable
 435+ this.saveAllButton = createButton( 'icon-tick',
 436+ mw.msg( 'inlinecategorizer-confirm-save-all' ),
 437+ '',
 438+ mw.msg( 'inlinecategorizer-confirm-save-all' )
 439+ );
 440+ this.cancelAllButton = createButton( 'icon-close',
 441+ mw.msg( 'inlinecategorizer-cancel-all' ),
 442+ '',
 443+ mw.msg( 'inlinecategorizer-cancel-all' )
 444+ );
 445+ this.saveAllButton.click( this.handleStashedCategories ).hide();
 446+ this.cancelAllButton.click( function() {
 447+ ajaxcat.resetAll( false );
 448+ } ).hide();
 449+ options.$containerNormal.append( this.saveAllButton ).append( this.cancelAllButton );
 450+ options.$container.append( this.addContainer );
 451+ },
 452+
 453+ /**
 454+ * Insert a newly added category into the DOM.
 455+ *
 456+ * @param catTitle {mw.Title} Category title for which a link should be created.
 457+ * @return {jQuery}
 458+ */
 459+ createCatLink: function( catTitle ) {
 460+ var catName = catTitle.getMainText(),
 461+ $catLinkWrapper = $( this.options.catLinkWrapper ),
 462+ $anchor = $( '<a>' )
 463+ .text( catName )
 464+ .attr( {
 465+ target: '_blank',
 466+ href: catTitle.getUrl()
 467+ } );
 468+
 469+ $catLinkWrapper.append( $anchor );
 470+
 471+ this.createCatButtons( $anchor );
 472+
 473+ return $anchor;
 474+ },
 475+
 476+ /**
 477+ * Create a suggestion box for use in edit/add dialogs
 478+ * @param prefill {String} Prefill input
 479+ * @param callback {Function} Called on submit
 480+ * @param buttonVal {String} Button text
 481+ */
 482+ makeSuggestionBox: function( prefill, callback, buttonVal ) {
 483+ // Create add category prompt
 484+ var $promptContainer = $( '<div class="mw-addcategory-prompt"></div>' ),
 485+ $promptTextbox = $( '<input type="text" size="30" class="mw-addcategory-input"></input>' ),
 486+ $addButton = $( '<input type="button" class="mw-addcategory-button"></input>' ),
 487+ ajaxcat = this;
 488+
 489+ if ( prefill !== '' ) {
 490+ $promptTextbox.val( prefill );
 491+ }
 492+
 493+ $addButton
 494+ .val( buttonVal )
 495+ .click( callback );
 496+
 497+ $promptTextbox
 498+ .keyup( function( e ) {
 499+ if ( e.keyCode === 13 ) {
 500+ $addButton.click();
 501+ }
 502+ } )
 503+ .suggestions( {
 504+ fetch: fetchSuggestions,
 505+ cancel: function() {
 506+ var req = this.data( 'suggestions-request' );
 507+ // XMLHttpRequest.abort is unimplemented in IE6, also returns nonstandard value of 'unknown' for typeof
 508+ if ( req && typeof req.abort !== 'unknown' && typeof req.abort !== 'undefined' && req.abort ) {
 509+ req.abort();
 510+ }
 511+ }
 512+ } )
 513+ .suggestions();
 514+
 515+ $promptContainer
 516+ .append( $promptTextbox )
 517+ .append( $addButton );
 518+
 519+ return $promptContainer;
 520+ },
 521+
 522+ /**
 523+ * Execute or queue a category addition.
 524+ *
 525+ * @param $link {jQuery} Anchor tag of category link inside #catlinks.
 526+ * @param catTitle {mw.Title} Instance of mw.Title of the category to be added.
 527+ * @param catSortkey {String} sort key (optional)
 528+ * @param noAppend
 529+ * @return {mw.inlineCategorizer}
 530+ */
 531+ handleCategoryAdd: function( $link, catTitle, catSortkey, noAppend ) {
 532+ var ajaxcat = this,
 533+ // Suffix is wikitext between '[[Category:Foo' and ']]'.
 534+ suffix = catSortkey ? '|' + catSortkey : '',
 535+ catName = catTitle.getMainText(),
 536+ catFull = catTitle.toText();
 537+
 538+ if ( this.containsCat( catName ) ) {
 539+ this.showError( mw.msg( 'inlinecategorizer-category-already-present', catName ) );
 540+ return this;
 541+ }
 542+
 543+ if ( !$link.length ) {
 544+ $link = this.createCatLink( catTitle );
 545+ }
 546+
 547+ // Mark red if missing
 548+ $link[(catTitle.exists() === false ? 'addClass' : 'removeClass')]( 'new' );
 549+
 550+ this.doConfirmEdit( {
 551+ modFn: function( oldText ) {
 552+ var newText = ajaxcat.runHooks( oldText, 'beforeAdd', catName );
 553+ newText = newText + "\n[[" + catFull + suffix + "]]\n";
 554+ return ajaxcat.runHooks( newText, 'afterAdd', catName );
 555+ },
 556+ dialogDescription: mw.message( 'inlinecategorizer-add-category-summary', catName ).escaped(),
 557+ editSummary: '+[[' + catFull + ']]',
 558+ doneFn: function( unsaved ) {
 559+ if ( !noAppend ) {
 560+ ajaxcat.options.$container
 561+ .find( '#mw-normal-catlinks > .mw-addcategory-prompt' ).children( 'input' ).hide();
 562+ ajaxcat.options.$container
 563+ .find( '#mw-normal-catlinks ul' ).append( $link.parent() );
 564+ } else {
 565+ // Remove input box & button
 566+ $link.data( 'deleteButton' ).click();
 567+
 568+ // Update link text and href
 569+ $link.show().text( catName ).attr( 'href', catTitle.getUrl() );
 570+ }
 571+ if ( unsaved ) {
 572+ $link.addClass( 'mw-added-category' );
 573+ }
 574+ $( '.mw-ajax-addcategory' ).click();
 575+ },
 576+ $link: $link,
 577+ action: 'add'
 578+ } );
 579+ return this;
 580+ },
 581+
 582+ /**
 583+ * Execute or queue a category edit.
 584+ *
 585+ * @param $link {jQuery} Anchor tag of category link in #catlinks.
 586+ * @param oldCatName {String} Name of category before edit
 587+ * @param catTitle {mw.Title} Instance of mw.Title for new category
 588+ * @param catSortkey {String} Sort key of new category link (optional)
 589+ * @param isAdded {Boolean} True if this is a new link, false if it changed an existing one
 590+ */
 591+ handleCategoryEdit: function( $link, oldCatName, catTitle, catSortkey, isAdded ) {
 592+ var ajaxcat = this,
 593+ catName = catTitle.getMainText();
 594+
 595+ // Category add needs to be handled differently
 596+ if ( isAdded ) {
 597+ // Pass sortkey back
 598+ this.handleCategoryAdd( $link, catTitle, catSortkey, true );
 599+ return;
 600+ }
 601+
 602+ // User didn't change anything, trigger delete
 603+ // @todo Document why it's deleted.
 604+ if ( oldCatName === catName ) {
 605+ $link.data( 'deleteButton' ).click();
 606+ return;
 607+ }
 608+
 609+ // Mark red if missing
 610+ $link[(catTitle.exists() === false ? 'addClass' : 'removeClass')]( 'new' );
 611+
 612+ var categoryRegex = buildRegex( oldCatName ),
 613+ editSummary = '[[' + new mw.Title( oldCatName, catNsId ).toText() + ']] -> [[' + catTitle.toText() + ']]';
 614+
 615+ ajaxcat.doConfirmEdit({
 616+ modFn: function( oldText ) {
 617+ var newText = ajaxcat.runHooks( oldText, 'beforeChange', oldCatName, catName ),
 618+ matches = newText.match( categoryRegex );
 619+
 620+ // Old cat wasn't found, likely to be transcluded
 621+ if ( !$.isArray( matches ) ) {
 622+ ajaxcat.showError( mw.msg( 'inlinecategorizer-edit-category-error' ) );
 623+ return false;
 624+ }
 625+
 626+ var suffix = catSortkey ? '|' + catSortkey : matches[0].replace( categoryRegex, '$2' ),
 627+ newCategoryWikitext = '[[' + catTitle + suffix + ']]';
 628+
 629+ if ( matches.length > 1 ) {
 630+ // The category is duplicated. Remove all but one match
 631+ for ( var i = 1; i < matches.length; i++ ) {
 632+ oldText = oldText.replace( matches[i], '' );
 633+ }
 634+ }
 635+ newText = oldText.replace( categoryRegex, newCategoryWikitext );
 636+
 637+ return ajaxcat.runHooks( newText, 'afterChange', oldCatName, catName );
 638+ },
 639+ dialogDescription: mw.message( 'inlinecategorizer-edit-category-summary', oldCatName, catName ).escaped(),
 640+ editSummary: editSummary,
 641+ doneFn: function( unsaved ) {
 642+ // Remove input box & button
 643+ $link.data( 'deleteButton' ).click();
 644+
 645+ // Update link text and href
 646+ $link.show().text( catName ).attr( 'href', catTitle.getUrl() );
 647+ if ( unsaved ) {
 648+ $link.data( 'origCat', oldCatName ).addClass( 'mw-changed-category' );
 649+ }
 650+ },
 651+ $link: $link,
 652+ action: 'edit'
 653+ });
 654+ },
 655+
 656+ /**
 657+ * Checks the API whether the category in question is a redirect.
 658+ * Also returns existance info (to color link red/blue)
 659+ * @param category {String} Name of category to resolve
 660+ * @param callback {Function} Called with 1 argument (mw.Title object)
 661+ */
 662+ resolveRedirects: function( category, callback ) {
 663+ if ( !this.options.resolveRedirects ) {
 664+ callback( category, true );
 665+ return;
 666+ }
 667+ var catTitle = new mw.Title( category, catNsId ),
 668+ queryVars = {
 669+ action:'query',
 670+ titles: catTitle.toString(),
 671+ redirects: 1,
 672+ format: 'json'
 673+ };
 674+
 675+ $.getJSON( mw.util.wikiScript( 'api' ), queryVars, function( json ) {
 676+ var redirect = json.query.redirects,
 677+ exists = !json.query.pages[-1];
 678+
 679+ // If it's a redirect 'exists' is for the target, not the origin
 680+ if ( redirect ) {
 681+ // Register existance of redirect origin as well,
 682+ // a non-existent page can't be a redirect.
 683+ mw.Title.exist.set( catTitle.toString(), true );
 684+
 685+ // Override title with the redirect target
 686+ catTitle = new mw.Title( redirect[0].to ).getMainText();
 687+ }
 688+
 689+ // Register existence
 690+ mw.Title.exist.set( catTitle.toString(), exists );
 691+
 692+ callback( catTitle );
 693+ } );
 694+ },
 695+
 696+ /**
 697+ * Append edit and remove buttons to a given category link
 698+ *
 699+ * @param DOMElement element Anchor element, to which the buttons should be appended.
 700+ * @return {mw.inlineCategorizer}
 701+ */
 702+ createCatButtons: function( $element ) {
 703+ var deleteButton = createButton( 'icon-close', mw.msg( 'inlinecategorizer-remove-category' ) ),
 704+ editButton = createButton( 'icon-edit', mw.msg( 'inlinecategorizer-edit-category' ) ),
 705+ saveButton = createButton( 'icon-tick', mw.msg( 'inlinecategorizer-confirm-save' ) ).hide(),
 706+ ajaxcat = this;
 707+
 708+ deleteButton.click( this.handleDeleteLink );
 709+ editButton.click( ajaxcat.createEditInterface );
 710+
 711+ $element.after( deleteButton ).after( editButton );
 712+
 713+ // Save references to all links and buttons
 714+ $element.data( {
 715+ deleteButton: deleteButton,
 716+ editButton: editButton,
 717+ saveButton: saveButton
 718+ } );
 719+ editButton.data( {
 720+ link: $element
 721+ } );
 722+ return this;
 723+ },
 724+
 725+ /**
 726+ * Append spinner wheel to element.
 727+ * @param $el {jQuery}
 728+ * @return {mw.inlineCategorizer}
 729+ */
 730+ addProgressIndicator: function( $el ) {
 731+ $el.append( $( '<div>' ).addClass( 'mw-ajax-loader' ) );
 732+ return this;
 733+ },
 734+
 735+ /**
 736+ * Find and remove spinner wheel from inside element.
 737+ * @param $el {jQuery}
 738+ * @return {mw.inlineCategorizer}
 739+ */
 740+ removeProgressIndicator: function( $el ) {
 741+ $el.find( '.mw-ajax-loader' ).remove();
 742+ return this;
 743+ },
 744+
 745+ /**
 746+ * Parse the DOM $container and build a list of
 747+ * present categories.
 748+ *
 749+ * @return {Array} All categories.
 750+ */
 751+ getCats: function() {
 752+ var cats = this.options.$container
 753+ .find( this.options.categoryLinkSelector )
 754+ .map( function() {
 755+ return $.trim( $( this ).text() );
 756+ } );
 757+ return cats;
 758+ },
 759+
 760+ /**
 761+ * Check whether a passed category is present in the DOM.
 762+ *
 763+ * @param newCat {String} Category name to be checked for.
 764+ * @return {Boolean}
 765+ */
 766+ containsCat: function( newCat ) {
 767+ newCat = $.ucFirst( newCat );
 768+ var match = false;
 769+ $.each( this.getCats(), function(i, cat) {
 770+ if ( $.ucFirst( cat ) === newCat ) {
 771+ match = true;
 772+ // Stop once we have a match
 773+ return false;
 774+ }
 775+ } );
 776+ return match;
 777+ },
 778+
 779+ /**
 780+ * Execute or queue a category delete.
 781+ *
 782+ * @param $link {jQuery}
 783+ * @param category
 784+ * @return ?
 785+ */
 786+ handleCategoryDelete: function( $link, category ) {
 787+ var categoryRegex = buildRegex( category, true ),
 788+ ajaxcat = this;
 789+
 790+ this.doConfirmEdit({
 791+ modFn: function( oldText ) {
 792+ var newText = ajaxcat.runHooks( oldText, 'beforeDelete', category );
 793+ newText = newText.replace( categoryRegex, '' );
 794+
 795+ if ( newText === oldText ) {
 796+ ajaxcat.showError( mw.msg( 'inlinecategorizer-remove-category-error' ) );
 797+ return false;
 798+ }
 799+
 800+ return ajaxcat.runHooks( newText, 'afterDelete', category );
 801+ },
 802+ dialogDescription: mw.message( 'inlinecategorizer-remove-category-summary', category ).escaped(),
 803+ editSummary: '-[[' + new mw.Title( category, catNsId ) + ']]',
 804+ doneFn: function( unsaved ) {
 805+ if ( unsaved ) {
 806+ $link.addClass( 'mw-removed-category' );
 807+ } else {
 808+ $link.parent().remove();
 809+ }
 810+ },
 811+ $link: $link,
 812+ action: 'delete'
 813+ });
 814+ },
 815+
 816+ /**
 817+ * Takes a category link element
 818+ * and strips all data from it.
 819+ *
 820+ * @param $link {jQuery}
 821+ * @param del {Boolean}
 822+ * @param dontRestoreText {Boolean}
 823+ * @return ?
 824+ */
 825+ resetCatLink: function( $link, del, dontRestoreText ) {
 826+ $link.removeClass( 'mw-removed-category mw-added-category mw-changed-category' );
 827+ var data = $link.data();
 828+
 829+ if ( typeof data.stashIndex === 'number' ) {
 830+ this.removeStashItem( data.stashIndex );
 831+ }
 832+ if ( del ) {
 833+ $link.parent().remove();
 834+ return;
 835+ }
 836+ if ( data.origCat && !dontRestoreText ) {
 837+ var catTitle = new mw.Title( data.origCat, catNsId );
 838+ $link.text( catTitle.getMainText() );
 839+ $link.attr( 'href', catTitle.getUrl() );
 840+ }
 841+
 842+ $link.removeData();
 843+
 844+ // Re-add data
 845+ $link.data( {
 846+ saveButton: data.saveButton,
 847+ deleteButton: data.deleteButton,
 848+ editButton: data.editButton
 849+ } );
 850+ },
 851+
 852+ /**
 853+ * Do the actual edit.
 854+ * Gets token & text from api, runs it through fn
 855+ * and saves it with summary.
 856+ * @param page {String} Pagename
 857+ * @param fn {Function} edit function
 858+ * @param summary {String}
 859+ * @param doneFn {String} Callback after all is done
 860+ */
 861+ doEdit: function( page, fn, summary, doneFn ) {
 862+ // Get an edit token for the page.
 863+ var getTokenVars = {
 864+ action: 'query',
 865+ prop: 'info|revisions',
 866+ intoken: 'edit',
 867+ titles: page,
 868+ rvprop: 'content|timestamp',
 869+ format: 'json'
 870+ }, ajaxcat = this;
 871+
 872+ $.post(
 873+ mw.util.wikiScript( 'api' ),
 874+ getTokenVars,
 875+ function( json ) {
 876+ if ( 'error' in json ) {
 877+ ajaxcat.showError( mw.msg( 'inlinecategorizer-api-error', json.error.code, json.error.info ) );
 878+ return;
 879+ } else if ( json.query && json.query.pages ) {
 880+ var infos = json.query.pages;
 881+ } else {
 882+ ajaxcat.showError( mw.msg( 'inlinecategorizer-api-unknown-error' ) );
 883+ return;
 884+ }
 885+
 886+ $.each( infos, function( pageid, data ) {
 887+ var token = data.edittoken,
 888+ timestamp = data.revisions[0].timestamp,
 889+ oldText = data.revisions[0]['*'],
 890+ nowikiKey = generateRandomId(), // Unique ID for nowiki replacement
 891+ nowikiFragments = []; // Nowiki fragments will be stored here during the changes
 892+
 893+ // Replace all nowiki parts with unique keys..
 894+ oldText = replaceNowikis( oldText, nowikiKey, nowikiFragments );
 895+
 896+ // ..then apply the changes to the page text..
 897+ var newText = fn( oldText );
 898+ if ( newText === false ) {
 899+ return;
 900+ }
 901+
 902+ // ..and restore the nowiki parts back.
 903+ newText = restoreNowikis( newText, nowikiKey, nowikiFragments );
 904+
 905+ var postEditVars = {
 906+ action: 'edit',
 907+ title: page,
 908+ text: newText,
 909+ summary: summary,
 910+ token: token,
 911+ basetimestamp: timestamp,
 912+ format: 'json'
 913+ };
 914+
 915+ $.post(
 916+ mw.util.wikiScript( 'api' ),
 917+ postEditVars,
 918+ doneFn,
 919+ 'json'
 920+ )
 921+ .error( function( xhr, text, error ) {
 922+ ajaxcat.showError( mw.msg( 'inlinecategorizer-api-error', text, error ) );
 923+ });
 924+ } );
 925+ },
 926+ 'json'
 927+ ).error( function( xhr, text, error ) {
 928+ ajaxcat.showError( mw.msg( 'inlinecategorizer-api-error', text, error ) );
 929+ } );
 930+ },
 931+
 932+ /**
 933+ * This gets called by all action buttons
 934+ * Displays a dialog to confirm the action
 935+ * Afterwards do the actual edit.
 936+ *
 937+ * @param props {Object}:
 938+ * - modFn {Function} text-modifying function
 939+ * - dialogDescription {String} Changes done (HTML for in the dialog, escape before hand if needed)
 940+ * - editSummary {String} Changes done (text for the edit summary)
 941+ * - doneFn {Function} callback after everything is done
 942+ * - $link {jQuery}
 943+ * - action
 944+ * @return {mw.inlineCategorizer}
 945+ */
 946+ doConfirmEdit: function( props ) {
 947+ var summaryHolder, reasonBox, dialog, submitFunction,
 948+ buttons = {},
 949+ dialogOptions = {
 950+ AutoOpen: true,
 951+ buttons: buttons,
 952+ width: 450
 953+ },
 954+ ajaxcat = this;
 955+
 956+ // Check whether to use multiEdit mode:
 957+ if ( this.options.multiEdit && props.action !== 'all' ) {
 958+
 959+ // Stash away
 960+ props.$link
 961+ .data( 'stashIndex', this.stash.fns.length )
 962+ .data( 'summary', props.dialogDescription );
 963+
 964+ this.stash.dialogDescriptions.push( props.dialogDescription );
 965+ this.stash.editSummaries.push( props.editSummary );
 966+ this.stash.fns.push( props.modFn );
 967+
 968+ this.saveAllButton.show();
 969+ this.cancelAllButton.show();
 970+
 971+ // Clear input field after action
 972+ ajaxcat.addContainer.find( '.mw-addcategory-input' ).val( '' );
 973+
 974+ // This only does visual changes, fire done and return.
 975+ props.doneFn( true );
 976+ return this;
 977+ }
 978+
 979+ // Summary of the action to be taken
 980+ summaryHolder = $( '<p>' )
 981+ .html( '<strong>' + mw.message( 'inlinecategorizer-category-question' ).escaped() + '</strong><br/>' + props.dialogDescription );
 982+
 983+ // Reason textbox.
 984+ reasonBox = $( '<input type="text" size="45"></input>' )
 985+ .addClass( 'mw-ajax-confirm-reason' );
 986+
 987+ // Produce a confirmation dialog
 988+ dialog = $( '<div>' )
 989+ .addClass( 'mw-ajax-confirm-dialog' )
 990+ .attr( 'title', mw.msg( 'inlinecategorizer-confirm-title' ) )
 991+ .append( summaryHolder )
 992+ .append( reasonBox );
 993+
 994+ // Submit button
 995+ submitFunction = function() {
 996+ ajaxcat.addProgressIndicator( dialog );
 997+ ajaxcat.doEdit(
 998+ mw.config.get( 'wgPageName' ),
 999+ props.modFn,
 1000+ props.editSummary + ': ' + reasonBox.val(),
 1001+ function() {
 1002+ props.doneFn();
 1003+
 1004+ // Clear input field after successful edit
 1005+ ajaxcat.addContainer.find( '.mw-addcategory-input' ).val( '' );
 1006+
 1007+ dialog.dialog( 'close' );
 1008+ ajaxcat.removeProgressIndicator( dialog );
 1009+ }
 1010+ );
 1011+ };
 1012+
 1013+ buttons[mw.msg( 'inlinecategorizer-confirm-save' )] = submitFunction;
 1014+
 1015+ dialog.dialog( dialogOptions ).keyup( function( e ) {
 1016+ // Close on enter
 1017+ if ( e.keyCode === 13 ) {
 1018+ submitFunction();
 1019+ }
 1020+ } );
 1021+
 1022+ return this;
 1023+ },
 1024+
 1025+ /**
 1026+ * @param index {Number|jQuery} Stash index or jQuery object of stash item.
 1027+ * @return {mw.inlineCategorizer}
 1028+ */
 1029+ removeStashItem: function( i ) {
 1030+ if ( typeof i !== 'number' ) {
 1031+ i = i.data( 'stashIndex' );
 1032+ }
 1033+
 1034+ try {
 1035+ delete this.stash.fns[i];
 1036+ delete this.stash.dialogDescriptions[i];
 1037+ } catch(e) {}
 1038+
 1039+ if ( $.isEmpty( this.stash.fns ) ) {
 1040+ this.stash.fns = [];
 1041+ this.stash.dialogDescriptions = [];
 1042+ this.stash.editSummaries = [];
 1043+ this.saveAllButton.hide();
 1044+ this.cancelAllButton.hide();
 1045+ }
 1046+ return this;
 1047+ },
 1048+
 1049+ /**
 1050+ * Reset all data from the category links and the stash.
 1051+ *
 1052+ * @param del {Boolean} Delete any category links with .mw-removed-category
 1053+ * @return {mw.inlineCategorizer}
 1054+ */
 1055+ resetAll: function( del ) {
 1056+ var $links = this.options.$container.find( this.options.categoryLinkSelector ),
 1057+ $del = $([]),
 1058+ ajaxcat = this;
 1059+
 1060+ if ( del ) {
 1061+ $del = $links.filter( '.mw-removed-category' ).parent();
 1062+ }
 1063+
 1064+ $links.each( function() {
 1065+ ajaxcat.resetCatLink( $( this ), false, del );
 1066+ } );
 1067+
 1068+ $del.remove();
 1069+
 1070+ this.options.$container.find( '#mw-hidden-catlinks' ).remove();
 1071+
 1072+ return this;
 1073+ },
 1074+
 1075+ /**
 1076+ * Add hooks
 1077+ * Currently available: beforeAdd, beforeChange, beforeDelete,
 1078+ * afterAdd, afterChange, afterDelete
 1079+ * If the hook function returns false, all changes are aborted.
 1080+ *
 1081+ * @param string type Type of hook to add
 1082+ * @param function fn Hook function. The following vars are passed to it:
 1083+ * 1. oldtext: The wikitext before the hook
 1084+ * 2. category: The deleted, added, or changed category
 1085+ * 3. (only for beforeChange/afterChange): newcategory
 1086+ */
 1087+ addHook: function( type, fn ) {
 1088+ if ( !this.hooks[type] || !$.isFunction( fn ) ) {
 1089+ return;
 1090+ }
 1091+ else {
 1092+ this.hooks[type].push( fn );
 1093+ }
 1094+ },
 1095+
 1096+
 1097+ /**
 1098+ * Open a dismissable error dialog
 1099+ *
 1100+ * @param string str The error description
 1101+ */
 1102+ showError: function( str ) {
 1103+ var oldDialog = $( '.mw-ajax-confirm-dialog' ),
 1104+ buttons = {},
 1105+ dialogOptions = {
 1106+ buttons: buttons,
 1107+ AutoOpen: true,
 1108+ title: mw.msg( 'inlinecategorizer-error-title' )
 1109+ };
 1110+
 1111+ this.removeProgressIndicator( oldDialog );
 1112+ oldDialog.dialog( 'close' );
 1113+
 1114+ var dialog = $( '<div>' ).text( str );
 1115+
 1116+ mw.util.$content.append( dialog );
 1117+
 1118+ buttons[mw.msg( 'inlinecategorizer-confirm-ok' )] = function( e ) {
 1119+ dialog.dialog( 'close' );
 1120+ };
 1121+
 1122+ dialog.dialog( dialogOptions ).keyup( function( e ) {
 1123+ if ( e.keyCode === 13 ) {
 1124+ dialog.dialog( 'close' );
 1125+ }
 1126+ } );
 1127+ },
 1128+
 1129+ /**
 1130+ * @param oldtext
 1131+ * @param type
 1132+ * @param category
 1133+ * @param categoryNew
 1134+ * @return oldtext
 1135+ */
 1136+ runHooks: function( oldtext, type, category, categoryNew ) {
 1137+ // No hooks registered
 1138+ if ( !this.hooks[type] ) {
 1139+ return oldtext;
 1140+ } else {
 1141+ for ( var i = 0; i < this.hooks[type].length; i++ ) {
 1142+ oldtext = this.hooks[type][i]( oldtext, category, categoryNew );
 1143+ if ( oldtext === false ) {
 1144+ this.showError( mw.msg( 'inlinecategorizer-category-hook-error', category ) );
 1145+ return;
 1146+ }
 1147+ }
 1148+ return oldtext;
 1149+ }
 1150+ }
 1151+};
 1152+
 1153+} )( jQuery );
Property changes on: trunk/extensions/InlineCategorizer/modules/ext.inlineCategorizer.core.js
___________________________________________________________________
Added: svn:mergeinfo
11154 Merged /branches/REL1_15/phase3/js2/ajaxcategories.js:r51646
21155 Merged /branches/sqlite/js2/ajaxcategories.js:r58211-58321
Added: svn:eol-style
31156 + native

Follow-up revisions

RevisionCommit summaryAuthorDate
r96251Normalise i18n file.siebrand20:31, 4 September 2011
r96252Delete uppercased filesreedy20:32, 4 September 2011
r96253Follow-up r96250: Add suppor for Inline Categorizer to Translate.siebrand20:32, 4 September 2011
r96254Follow-up r96250: Remove messages from all core language files and populate I...siebrand20:38, 4 September 2011

Status & tagging log