Split ajaxCategories away from core for now, into an extension:
authorKrinkle <krinkle@users.mediawiki.org>
Sun, 4 Sep 2011 19:35:22 +0000 (19:35 +0000)
committerKrinkle <krinkle@users.mediawiki.org>
Sun, 4 Sep 2011 19:35:22 +0000 (19:35 +0000)
* 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.

19 files changed:
includes/DefaultSettings.php
includes/OutputPage.php
languages/messages/MessagesEn.php
languages/messages/MessagesQqq.php
maintenance/language/messages.inc
resources/Resources.php
resources/mediawiki.page/images/ajaxcat-add-hover.png [deleted file]
resources/mediawiki.page/images/ajaxcat-add.png [deleted file]
resources/mediawiki.page/images/ajaxcat-close-hover.png [deleted file]
resources/mediawiki.page/images/ajaxcat-close.png [deleted file]
resources/mediawiki.page/images/ajaxcat-edit-hover.png [deleted file]
resources/mediawiki.page/images/ajaxcat-edit.png [deleted file]
resources/mediawiki.page/images/ajaxcat-error-hover.png [deleted file]
resources/mediawiki.page/images/ajaxcat-error.png [deleted file]
resources/mediawiki.page/images/ajaxcat-tick-hover.png [deleted file]
resources/mediawiki.page/images/ajaxcat-tick.png [deleted file]
resources/mediawiki.page/mediawiki.page.ajaxCategories.css [deleted file]
resources/mediawiki.page/mediawiki.page.ajaxCategories.init.js [deleted file]
resources/mediawiki.page/mediawiki.page.ajaxCategories.js [deleted file]

index 972e466..23cdb6e 100644 (file)
@@ -5619,19 +5619,6 @@ $wgSeleniumConfigFile = null;
 $wgDBtestuser = ''; //db user that has permission to create and drop the test databases only
 $wgDBtestpassword = '';
 
-/**
- * Whether or not to use the AJAX categories system.
- */
-$wgUseAJAXCategories = false;
-
-/**
- * Only enable AJAXCategories on configured namespaces. Default is all.
- *
- * Example:
- *   $wgAJAXCategoriesNamespaces = array( NS_MAIN, NS_PROJECT );
- */
-$wgAJAXCategoriesNamespaces = array();
-
 /**
  * For really cool vim folding this needs to be at the end:
  * vim: foldmarker=@{,@} foldmethod=marker
index a45a3ad..e81ec92 100644 (file)
@@ -2296,8 +2296,7 @@ $distantTemplates
         * Add the default ResourceLoader modules to this object
         */
        private function addDefaultModules() {
-               global $wgIncludeLegacyJavaScript, $wgUseAjax,
-                       $wgAjaxWatch, $wgEnableMWSuggest, $wgUseAJAXCategories;
+               global $wgIncludeLegacyJavaScript, $wgUseAjax, $wgAjaxWatch, $wgEnableMWSuggest;
 
                // Add base resources
                $this->addModules( array(
@@ -2333,16 +2332,6 @@ $distantTemplates
                if ( $this->isArticle() && $this->getUser()->getOption( 'editondblclick' ) ) {
                        $this->addModules( 'mediawiki.action.view.dblClickEdit' );
                }
-
-               if ( $wgUseAJAXCategories ) {
-                       global $wgAJAXCategoriesNamespaces;
-
-                       $title = $this->getTitle();
-
-                       if( empty( $wgAJAXCategoriesNamespaces ) || in_array( $title->getNamespace(), $wgAJAXCategoriesNamespaces ) ) {
-                               $this->addModules( 'mediawiki.page.ajaxCategories.init' );
-                       }
-               }
        }
 
        /**
index 66cf503..6755f6c 100644 (file)
@@ -4636,29 +4636,4 @@ This site is experiencing technical difficulties.',
 'sqlite-has-fts' => '$1 with full-text search support',
 'sqlite-no-fts'  => '$1 without full-text search support',
 
-# Add categories per AJAX
-'ajax-add-category'             => 'Add category',
-'ajax-remove-category'          => 'Remove category',
-'ajax-edit-category'            => 'Edit category',
-'ajax-add-category-submit'      => 'Add',
-'ajax-confirm-ok'               => 'OK',
-'ajax-confirm-title'            => 'Confirm action',
-'ajax-confirm-save'             => 'Save',
-'ajax-confirm-save-all'         => 'Save all changes',
-'ajax-cancel'                   => 'Cancel edit',
-'ajax-cancel-all'               => 'Cancel all changes',
-'ajax-add-category-summary'     => 'Add category "$1"',
-'ajax-edit-category-summary'    => 'Change category "$1" to "$2"',
-'ajax-remove-category-summary'  => 'Remove category "$1"',
-'ajax-category-question'        => 'Why do you want to make the following changes:',
-'ajax-error-title'              => 'Error',
-'ajax-remove-category-error'    => 'It was not possible to remove category "$1".
-This usually occurs when the category has been added to the page in a template.',
-'ajax-edit-category-error'      => 'It was not possible to edit category "$1".
-This usually occurs when the category has been added to the page in a template.',
-'ajax-category-already-present' => 'This page already belongs to the category "$1"',
-'ajax-category-hook-error'      => 'A local function prevented the changes from being saved.',
-'ajax-api-error'                => 'The API returned an error: $1: $2.',
-'ajax-api-unknown-error'        => 'The API returned an unknown error.',
-
 );
index c95ee2a..6e82557 100644 (file)
@@ -4300,23 +4300,4 @@ The number of following wikis is unknown. For an empty result, no message is sho
 'sqlite-has-fts' => 'Shown on Special:Version, $1 is version',
 'sqlite-no-fts'  => 'Shown on Special:Version, $1 is version',
 
-# Add categories per AJAX
-'ajax-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.',
-'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.',
-'ajax-add-category-submit'      => '{{Identical|Add}}',
-'ajax-confirm-ok'               => '{{Identical|OK}}',
-'ajax-confirm-title'            => 'Title for a dialog box in which the user is asked for an edit summary',
-'ajax-confirm-save'             => 'Submit button {{Identical|Save}}',
-'ajax-confirm-save-all'         => 'Submit button to save all changes',
-'ajax-add-category-summary'     => 'See {{msg-mw|ajax-category-question}}. $1 is a category name.',
-'ajax-edit-category-summary'    => 'See {{msg-mw|ajax-category-question}}. $1 and $2 are both category names.',
-'ajax-remove-category-summary'  => 'See {{msg-mw|ajax-category-question}}. $1 is a category name.',
-'ajax-category-question'        => "Question the user is asked before submit. It's followed by a list of the changes.",
-'ajax-error-title'              => '{{Identical|Error}}',
-'ajax-category-already-present' => 'Error message. $1 is the category name',
-'ajax-api-error'                => 'API = [http://en.wikipedia.org/wiki/Application_programming_interface Application programming interface].
-
-"returned" here means "reported".',
-
 );
index 755cfc1..e6e6795 100644 (file)
@@ -3504,30 +3504,6 @@ $wgMessageStructure = array(
        'unwatch' => array(
                'confirm-unwatch-button',
        ),
-       'ajax-category' => array(
-               'ajax-add-category',
-               'ajax-remove-category',
-               'ajax-edit-category',
-               'ajax-add-category-submit',
-               'ajax-confirm-ok',
-               'ajax-confirm-title',
-               'ajax-confirm-save',
-               'ajax-confirm-save-all',
-               'ajax-cancel',
-               'ajax-cancel-all',
-               'ajax-add-category-summary',
-               'ajax-edit-category-summary',
-               'ajax-remove-category-summary',
-               'ajax-category-question',
-               'ajax-error-title',
-               'ajax-remove-category-error',
-               'ajax-edit-category-error',
-               'ajax-category-already-present',
-               'ajax-category-hook-error',
-               'ajax-api-error',
-               'ajax-api-unknown-error',
-       ),
-
 );
 
 /** Comments for each block */
@@ -3759,5 +3735,4 @@ Variants for Chinese language",
        'db-error-messages'     => 'Database error messages',
        'html-forms'            => 'HTML forms',
        'sqlite'                => 'SQLite database support',
-       'ajax-category'         => 'Add categories per AJAX',
 );
index 6613cb3..b5ceed1 100644 (file)
@@ -602,45 +602,6 @@ return array(
                        'jquery.mwExtension',
                ),
        ),
-       'mediawiki.page.ajaxCategories' => array(
-               'scripts' => 'resources/mediawiki.page/mediawiki.page.ajaxCategories.js',
-               'styles' => 'resources/mediawiki.page/mediawiki.page.ajaxCategories.css',
-               'dependencies' => array(
-                       'jquery.suggestions',
-                       'jquery.ui.dialog',
-                       'mediawiki.Title',
-               ),
-               'messages' => array(
-                       'ajax-add-category',
-                       'ajax-remove-category',
-                       'ajax-edit-category',
-                       'ajax-add-category-submit',
-                       'ajax-confirm-ok',
-                       'ajax-confirm-title',
-                       'ajax-confirm-save',
-                       'ajax-confirm-save-all',
-                       'ajax-cancel',
-                       'ajax-cancel-all',
-                       'ajax-add-category-summary',
-                       'ajax-edit-category-summary',
-                       'ajax-remove-category-summary',
-                       'ajax-category-question',
-                       'ajax-category-and',
-                       'ajax-error-title',
-                       'ajax-remove-category-error',
-                       'ajax-edit-category-error',
-                       'ajax-category-already-present',
-                       'ajax-category-hook-error',
-                       'ajax-api-error',
-                       'ajax-api-unknown-error',
-               ),
-       ),
-       'mediawiki.page.ajaxCategories.init' => array(
-               'scripts' => 'resources/mediawiki.page/mediawiki.page.ajaxCategories.init.js',
-               'dependencies' => array(
-                       'mediawiki.page.ajaxCategories',
-               ),
-       ),
        'mediawiki.page.ready' => array(
                'scripts' => 'resources/mediawiki.page/mediawiki.page.ready.js',
                'dependencies' => array(
diff --git a/resources/mediawiki.page/images/ajaxcat-add-hover.png b/resources/mediawiki.page/images/ajaxcat-add-hover.png
deleted file mode 100644 (file)
index 9815f4f..0000000
Binary files a/resources/mediawiki.page/images/ajaxcat-add-hover.png and /dev/null differ
diff --git a/resources/mediawiki.page/images/ajaxcat-add.png b/resources/mediawiki.page/images/ajaxcat-add.png
deleted file mode 100644 (file)
index 2c39d3b..0000000
Binary files a/resources/mediawiki.page/images/ajaxcat-add.png and /dev/null differ
diff --git a/resources/mediawiki.page/images/ajaxcat-close-hover.png b/resources/mediawiki.page/images/ajaxcat-close-hover.png
deleted file mode 100644 (file)
index 679459e..0000000
Binary files a/resources/mediawiki.page/images/ajaxcat-close-hover.png and /dev/null differ
diff --git a/resources/mediawiki.page/images/ajaxcat-close.png b/resources/mediawiki.page/images/ajaxcat-close.png
deleted file mode 100644 (file)
index 2aaef6c..0000000
Binary files a/resources/mediawiki.page/images/ajaxcat-close.png and /dev/null differ
diff --git a/resources/mediawiki.page/images/ajaxcat-edit-hover.png b/resources/mediawiki.page/images/ajaxcat-edit-hover.png
deleted file mode 100644 (file)
index afd03d9..0000000
Binary files a/resources/mediawiki.page/images/ajaxcat-edit-hover.png and /dev/null differ
diff --git a/resources/mediawiki.page/images/ajaxcat-edit.png b/resources/mediawiki.page/images/ajaxcat-edit.png
deleted file mode 100644 (file)
index 62592a2..0000000
Binary files a/resources/mediawiki.page/images/ajaxcat-edit.png and /dev/null differ
diff --git a/resources/mediawiki.page/images/ajaxcat-error-hover.png b/resources/mediawiki.page/images/ajaxcat-error-hover.png
deleted file mode 100644 (file)
index 30f9c7a..0000000
Binary files a/resources/mediawiki.page/images/ajaxcat-error-hover.png and /dev/null differ
diff --git a/resources/mediawiki.page/images/ajaxcat-error.png b/resources/mediawiki.page/images/ajaxcat-error.png
deleted file mode 100644 (file)
index 580450e..0000000
Binary files a/resources/mediawiki.page/images/ajaxcat-error.png and /dev/null differ
diff --git a/resources/mediawiki.page/images/ajaxcat-tick-hover.png b/resources/mediawiki.page/images/ajaxcat-tick-hover.png
deleted file mode 100644 (file)
index a1e0220..0000000
Binary files a/resources/mediawiki.page/images/ajaxcat-tick-hover.png and /dev/null differ
diff --git a/resources/mediawiki.page/images/ajaxcat-tick.png b/resources/mediawiki.page/images/ajaxcat-tick.png
deleted file mode 100644 (file)
index 2e7bd36..0000000
Binary files a/resources/mediawiki.page/images/ajaxcat-tick.png and /dev/null differ
diff --git a/resources/mediawiki.page/mediawiki.page.ajaxCategories.css b/resources/mediawiki.page/mediawiki.page.ajaxCategories.css
deleted file mode 100644 (file)
index bf1b95d..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-.mw-addcategory-prompt {
-       display: inline;
-}
-
-.mw-addcategory-prompt input {
-       margin-left: 0.5em;
-       margin-right: 0.5em;
-}
-
-.mw-remove-category {
-       padding: 2px 8px;
-       display: inline;
-}
-.mw-removed-category {
-       text-decoration: line-through;
-}
-
-#catlinks:hover .icon {
-       opacity: 1;
-}
-#catlinks ul {
-       margin-right: 2em;
-}
-
-.mw-ajax-addcategory-holder {
-       display: inline-block;
-}
-.mw-ajax-addcategory {
-       margin-right: 1em;
-       cursor: pointer;
-       display: inline-block;
-}
-
-#catlinks .icon {
-       cursor: pointer;
-       padding: 1px 8px;
-       margin: 0;
-       background-position: 0 0;
-       background-repeat: no-repeat;
-       opacity: 0.5;
-
-}
-#catlinks .icon-parent {
-       cursor: pointer;
-       margin-right: 1em;
-}
-#catlinks .icon-close {
-       /* @embed */ background-image: url(images/ajaxcat-close.png);
-}
-#catlinks .icon-edit {
-       /* @embed */ background-image: url(images/ajaxcat-edit.png);
-}
-#catlinks .icon-tick {
-       /* @embed */ background-image: url(images/ajaxcat-tick.png);
-}
-#catlinks .icon-add {
-       /* @embed */ background-image: url(images/ajaxcat-add.png);
-}
-#catlinks .icon-close:hover {
-       /* @embed */ background-image: url(images/ajaxcat-close-hover.png);
-}
-#catlinks .icon-edit:hover {
-       /* @embed */ background-image: url(images/ajaxcat-edit-hover.png);
-}
-#catlinks .icon-tick:hover {
-       /* @embed */ background-image: url(images/ajaxcat-tick-hover.png);
-}
-#catlinks .icon-add:hover {
-       /* @embed */ background-image: url(images/ajaxcat-add-hover.png);
-}
diff --git a/resources/mediawiki.page/mediawiki.page.ajaxCategories.init.js b/resources/mediawiki.page/mediawiki.page.ajaxCategories.init.js
deleted file mode 100644 (file)
index a90342f..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-mw.page.ajaxCategories = new mw.ajaxCategories();
-jQuery( document ).ready( function(){
-       // Separate function for call to prevent jQuery
-       // from executing it in the document context.
-       mw.page.ajaxCategories.setup();
-} );
diff --git a/resources/mediawiki.page/mediawiki.page.ajaxCategories.js b/resources/mediawiki.page/mediawiki.page.ajaxCategories.js
deleted file mode 100644 (file)
index 916ec6b..0000000
+++ /dev/null
@@ -1,1155 +0,0 @@
-/**
- * mediaWiki.page.ajaxCategories
- *
- * @author Michael Dale, 2009
- * @author Leo Koppelkamm, 2011
- * @author Timo Tijhof, 2011
- * @since 1.19
- *
- * Relies on: mw.config (wgFormattedNamespaces, wgNamespaceIds,
- * wgCaseSensitiveNamespaces, wgUserGroups), mw.util.wikiGetlink, mw.user.getId
- */
-( function( $ ) {
-
-       /* Local scope */
-
-       var     catNsId = mw.config.get( 'wgNamespaceIds' ).category,
-               defaultOptions = {
-                       catLinkWrapper: '<li>',
-                       $container: $( '.catlinks' ),
-                       $containerNormal: $( '#mw-normal-catlinks' ),
-                       categoryLinkSelector: 'li a:not(.icon)',
-                       multiEdit: $.inArray( 'user', mw.config.get( 'wgUserGroups' ) ) !== -1,
-                       resolveRedirects: true
-               },
-               isCatNsSensitive = $.inArray( 14, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1;
-
-       /**
-        * @return {String}
-        */
-       function clean( s ) {
-               if ( typeof s === 'string' ) {
-                       return s.replace( /[\x00-\x1f\x23\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f\s]+/g, '' );
-               }
-               return '';
-       }
-
-       /**
-       * Generates a random id out of 62 alpha-numeric characters.
-       *
-       * @param {Number} Length of id (optional, defaults to 32)
-       * @return {String}
-       */
-       function generateRandomId( idLength ) {
-               idLength = typeof idLength === 'number' ? idLength : 32;
-               var     seed = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
-                       id = '';
-               for ( var r, i = 0; i < idLength; i++ ) {
-                       r = Math.floor( Math.random() * seed.length );
-                       id += seed.substring( r, r + 1 );
-               }
-               return id;
-       }
-
-       /**
-        * Helper function for $.fn.suggestions
-        *
-        * @context {jQuery}
-        * @param value {String} Textbox value.
-        */
-       function fetchSuggestions( value ) {
-               var     request,
-                       $el = this,
-                       catName = clean( value );
-
-               request = $.ajax( {
-                       url: mw.util.wikiScript( 'api' ),
-                       data: {
-                               action: 'query',
-                               list: 'allpages',
-                               apnamespace: catNsId,
-                               apprefix: catName,
-                               format: 'json'
-                       },
-                       dataType: 'json',
-                       success: function( data ) {
-                               // Process data.query.allpages into an array of titles
-                               var     pages = data.query.allpages,
-                                       titleArr = $.map( pages, function( page ) {
-                                               return new mw.Title( page.title ).getMainText();
-                                       } );
-
-                               $el.suggestions( 'suggestions', titleArr );
-                       }
-               } );
-               $el.data( 'suggestions-request', request );
-       }
-
-       /**
-        * Replace <nowiki> and comments with unique keys in the page text.
-        *
-        * @param text {String}
-        * @param id {String} Unique key for this nowiki replacement layer call.
-        * @param keys {Array} Array where fragments will be stored in.
-        * @return {String}
-        */
-       function replaceNowikis( text, id, keys ) {
-               var matches = text.match( /(<nowiki\>[\s\S]*?<\/nowiki>|<\!--[\s\S]*?--\>)/g );
-               for ( var i = 0; matches && i < matches.length; i++ ) {
-                       keys[i] = matches[i];
-                       text = text.replace( matches[i], '' + id + '-' + i );
-               }
-               return text;
-       }
-
-       /**
-        * Restore <nowiki> and comments from unique keys in the page text.
-        *
-        * @param text {String}
-        * @param id {String} Unique key of the layer to be restored, as passed to replaceNowikis().
-        * @param keys {Array} Array where fragements should be fetched from.
-        * @return {String}
-        */
-       function restoreNowikis( text, id, keys ) {
-               for ( var i = 0; i < keys.length; i++ ) {
-                       text = text.replace( '' + id + '-' + i, keys[i] );
-               }
-               return text;
-       }
-
-       /**
-        * Makes regex string caseinsensitive.
-        * Useful when 'i' flag can't be used.
-        * Return stuff like [Ff][Oo][Oo]
-        *
-        * @param string {String} Regex string
-        * @return {String} Processed regex string
-        */
-       function makeCaseInsensitive( string ) {
-               var newString = '';
-               for ( var i = 0; i < string.length; i++ ) {
-                       newString += '[' + string.charAt( i ).toUpperCase() + string.charAt( i ).toLowerCase() + ']';
-               }
-               return newString;
-       }
-
-       /**
-        * Build a regex that matches legal invocations of the passed category.
-        * @param category {String}
-        * @param matchLineBreak {Boolean} Match one following linebreak as well?
-        * @return {RegExp}
-        */
-       function buildRegex( category, matchLineBreak ) {
-               var     categoryRegex, categoryNSFragment,
-                       titleFragment = $.escapeRE( category ).replace( /( |_)/g, '[ _]' ),
-                       firstChar = titleFragment.charAt( 0 );
-
-               // Filter out all names for category namespace
-               categoryNSFragment = $.map( mw.config.get( 'wgNamespaceIds' ), function( id, name ) {
-                       if ( id === catNsId ) {
-                               name = $.escapeRE( name );
-                               return !isCatNsSensitive ? makeCaseInsensitive( name ) : name;
-                       }
-                       // Otherwise don't include in categoryNSFragment
-                       return null;
-               } ).join( '|' );
-
-               firstChar = '[' + firstChar.toUpperCase() + firstChar.toLowerCase() + ']';
-               titleFragment = firstChar + titleFragment.substr( 1 );
-               categoryRegex = '\\[\\[(' + categoryNSFragment + ')' + '[ _]*' + ':' + '[ _]*' + titleFragment + '[ _]*' + '(\\|[^\\]]*)?\\]\\]';
-               if ( matchLineBreak ) {
-                       categoryRegex += '[ \\t\\r]*\\n?';
-               }
-               return new RegExp( categoryRegex, 'g' );
-       }
-
-       /**
-        * Manufacture iconed button, with or without text.
-        *
-        * @param icon {String} The icon class.
-        * @param title {String} Title attribute.
-        * @param className {String} (optional) Additional classes to be added to the button.
-        * @param text {String} (optional) Text label of button.
-        * @return {jQuery} The button.
-        */
-       function createButton( icon, title, className, text ){
-               // We're adding a zero width space for IE7, it's got problems with empty nodes apparently
-               var $button = $( '<a>' )
-                       .addClass( className || '' )
-                       .attr( 'title', title )
-                       .html( '&#8203;' );
-
-               if ( text ) {
-                       var $icon = $( '<span>' ).addClass( 'icon ' + icon ).html( '&#8203;' );
-                       $button.addClass( 'icon-parent' ).append( $icon ).append( mw.html.escape( text ) );
-               } else {
-                       $button.addClass( 'icon ' + icon );
-               }
-               return $button;
-       }
-
-/**
- * @constructor
- * @param
- */
-mw.ajaxCategories = function( options ) {
-
-       this.options = options = $.extend( defaultOptions, options );
-
-       // Save scope in shortcut
-       var     ajaxcat = this;
-
-       // Elements tied to this instance
-       this.saveAllButton = null;
-       this.cancelAllButton = null;
-       this.addContainer = null;
-
-       this.request = null;
-
-       // Stash and hooks
-       this.stash = {
-               dialogDescriptions: [],
-               editSummaries: [],
-               fns: []
-       };
-       this.hooks = {
-               beforeAdd: [],
-               beforeChange: [],
-               beforeDelete: [],
-               afterAdd: [],
-               afterChange: [],
-               afterDelete: []
-       };
-
-       /* Event handlers */
-
-       /**
-        * Handle add category submit. Not to be called directly.
-        *
-        * @context Element
-        * @param e {jQuery Event}
-        */
-       this.handleAddLink = function( e ) {
-               var     $el = $( this ),
-                       $link = $([]),
-                       categoryText = $.ucFirst( $el.parent().find( '.mw-addcategory-input' ).val() || '' );
-
-               // Resolve redirects
-               ajaxcat.resolveRedirects( categoryText, function( resolvedCatTitle ) {
-                       ajaxcat.handleCategoryAdd( $link, resolvedCatTitle, '', false );
-               } );
-       };
-
-       /**
-        * @context Element
-        * @param e {jQuery Event}
-        */
-       this.createEditInterface = function( e ) {
-               var $el = $( this ),
-                       $link = $el.data( 'link' ),
-                       category = $link.text(),
-                       $input = ajaxcat.makeSuggestionBox( category,
-                               ajaxcat.handleEditLink,
-                               ajaxcat.options.multiEdit ? mw.msg( 'ajax-confirm-ok' ) : mw.msg( 'ajax-confirm-save' )
-                       );
-
-               $link.after( $input ).hide();
-
-               $input.find( '.mw-addcategory-input' ).focus();
-
-               // Get the editButton associated with this category link,
-               // and hide it.
-               $link.data( 'editButton' ).hide();
-
-               // Get the deleteButton associated with this category link,
-               $link.data( 'deleteButton' )
-                       // (re)set click handler
-                       .unbind( 'click' )
-                       .click( function() {
-                               // When the delete button is clicked:
-                               // - Remove the suggestion box
-                               // - Show the link and it's edit button
-                               // - (re)set the click handler again
-                               $input.remove();
-                               $link.show().data( 'editButton' ).show();
-                               $( this )
-                                       .unbind( 'click' )
-                                       .click( ajaxcat.handleDeleteLink )
-                                       .attr( 'title', mw.msg( 'ajax-remove-category' ) );
-                       })
-                       .attr( 'title', mw.msg( 'ajax-cancel' ) );
-       };
-
-       /**
-        * Handle edit category submit. Not to be called directly.
-        *
-        * @context Element
-        * @param e {jQuery Event}
-        */
-       this.handleEditLink = function( e ) {
-               var     input, category, sortkey, categoryOld,
-                       $el = $( this ),
-                       $link = $el.parent().parent().find( 'a:not(.icon)' );
-
-               // Grab category text
-               input = $el.parent().find( '.mw-addcategory-input' ).val();
-
-               // Split categoryname and sortkey
-               var arr = input.split( '|', 2 );
-               category = arr[0];
-               sortkey = arr[1]; // Is usually undefined, ie. if there was no '|' in the input.
-
-               // Grab text
-               var isAdded = $link.hasClass( 'mw-added-category' );
-               ajaxcat.resetCatLink( $link );
-               categoryOld = $link.text();
-
-               // If something changed and the new cat is already on the page, delete it.
-               if ( categoryOld !== category && ajaxcat.containsCat( category ) ) {
-                       $link.data( 'deleteButton' ).click();
-                       return;
-               }
-
-               // Resolve redirects
-               ajaxcat.resolveRedirects( category, function( resolvedCatTitle ) {
-                       ajaxcat.handleCategoryEdit( $link, categoryOld, resolvedCatTitle, sortkey, isAdded );
-               });
-       };
-
-       /**
-        * Handle delete category submit. Not to be called directly.
-        *
-        * @context Element
-        * @param e {jQuery Event}
-        */
-       this.handleDeleteLink = function( e ) {
-               var     $el = $( this ),
-                       $link = $el.parent().find( 'a:not(.icon)' ),
-                       category = $link.text();
-
-               if ( $link.is( '.mw-added-category, .mw-changed-category' ) ) {
-                       // We're just cancelling the addition or edit
-                       ajaxcat.resetCatLink( $link, $link.hasClass( 'mw-added-category' ) );
-                       return;
-               } else if ( $link.is( '.mw-removed-category' ) ) {
-                       // It's already removed...
-                       return;
-               }
-               ajaxcat.handleCategoryDelete( $link, category );
-       };
-
-       /**
-        * When multiEdit mode is enabled,
-        * this is called when the user clicks "save all"
-        * Combines the dialogDescriptions and edit functions.
-        *
-        * @context Element
-        * @return ?
-        */
-       this.handleStashedCategories = function() {
-
-               // Remove "holes" in array
-               var dialogDescriptions = $.grep( ajaxcat.stash.dialogDescriptions, function( n, i ) {
-                       return n;
-               } );
-
-               if ( dialogDescriptions.length < 1 ) {
-                       // Nothing to do here.
-                       ajaxcat.saveAllButton.hide();
-                       ajaxcat.cancelAllButton.hide();
-                       return;
-               } else {
-                       dialogDescriptions = dialogDescriptions.join( '<br/>' );
-               }
-
-               // Remove "holes" in array
-               var summaryShort = $.grep( ajaxcat.stash.editSummaries, function( n,i ) {
-                       return n;
-               } );
-               summaryShort = summaryShort.join( ', ' );
-
-               var     fns = ajaxcat.stash.fns;
-
-               ajaxcat.doConfirmEdit( {
-                       modFn: function( oldtext ) {
-                               // Run the text through all action functions
-                               var newtext = oldtext;
-                               for ( var i = 0; i < fns.length; i++ ) {
-                                       if ( $.isFunction( fns[i] ) ) {
-                                               newtext = fns[i]( newtext );
-                                               if ( newtext === false ) {
-                                                       return false;
-                                               }
-                                       }
-                               }
-                               return newtext;
-                       },
-                       dialogDescription: dialogDescriptions,
-                       editSummary: summaryShort,
-                       doneFn: function() {
-                               ajaxcat.resetAll( true );
-                       },
-                       $link: null,
-                       action: 'all'
-               } );
-       };
-};
-
-/* Public methods */
-
-mw.ajaxCategories.prototype = {
-       /**
-        * Create the UI
-        */
-       setup: function() {
-               // Could be set by gadgets like HotCat etc.
-               if ( mw.config.get( 'disableAJAXCategories' ) ) {
-                       return false;
-               }
-               // Only do it for articles.
-               if ( !mw.config.get( 'wgIsArticle' ) ) {
-                       return;
-               }
-               var options = this.options,
-                       ajaxcat = this,
-                       // Create [Add Category] link
-                       $addLink = createButton( 'icon-add',
-                               mw.msg( 'ajax-add-category' ),
-                               'mw-ajax-addcategory',
-                               mw.msg( 'ajax-add-category' )
-                       ).click( function() {
-                               $( this ).nextAll().toggle().filter( '.mw-addcategory-input' ).focus();
-                       });
-
-               // Create add category prompt
-               this.addContainer = this.makeSuggestionBox( '', this.handleAddLink, mw.msg( 'ajax-add-category-submit' ) );
-               this.addContainer.children().hide();
-               this.addContainer.prepend( $addLink );
-
-               // Create edit & delete link for each category.
-               $( '#catlinks' ).find( 'li a' ).each( function() {
-                       ajaxcat.createCatButtons( $( this ) );
-               });
-
-               options.$containerNormal.append( this.addContainer );
-
-               // @todo Make more clickable
-               this.saveAllButton = createButton( 'icon-tick',
-                       mw.msg( 'ajax-confirm-save-all' ),
-                       '',
-                       mw.msg( 'ajax-confirm-save-all' )
-               );
-               this.cancelAllButton = createButton( 'icon-close',
-                       mw.msg( 'ajax-cancel-all' ),
-                       '',
-                       mw.msg( 'ajax-cancel-all' )
-               );
-               this.saveAllButton.click( this.handleStashedCategories ).hide();
-               this.cancelAllButton.click( function() {
-                       ajaxcat.resetAll( false );
-               } ).hide();
-               options.$containerNormal.append( this.saveAllButton ).append( this.cancelAllButton );
-               options.$container.append( this.addContainer );
-       },
-
-       /**
-        * Insert a newly added category into the DOM.
-        *
-        * @param catTitle {mw.Title} Category title for which a link should be created.
-        * @return {jQuery}
-        */
-       createCatLink: function( catTitle ) {
-               var     catName = catTitle.getMainText(),
-                       $catLinkWrapper = $( this.options.catLinkWrapper ),
-                       $anchor = $( '<a>' )
-                               .text( catName )
-                               .attr( {
-                                       target: '_blank',
-                                       href: catTitle.getUrl()
-                               } );
-
-               $catLinkWrapper.append( $anchor );
-
-               this.createCatButtons( $anchor );
-
-               return $anchor;
-       },
-
-       /**
-        * Create a suggestion box for use in edit/add dialogs
-        * @param prefill {String} Prefill input
-        * @param callback {Function} Called on submit
-        * @param buttonVal {String} Button text
-        */
-       makeSuggestionBox: function( prefill, callback, buttonVal ) {
-               // Create add category prompt
-               var     $promptContainer = $( '<div class="mw-addcategory-prompt"></div>' ),
-                       $promptTextbox = $( '<input type="text" size="30" class="mw-addcategory-input"></input>' ),
-                       $addButton = $( '<input type="button" class="mw-addcategory-button"></input>' ),
-                       ajaxcat = this;
-
-               if ( prefill !== '' ) {
-                       $promptTextbox.val( prefill );
-               }
-
-               $addButton
-                       .val( buttonVal )
-                       .click( callback );
-
-               $promptTextbox
-                       .keyup( function( e ) {
-                               if ( e.keyCode === 13 ) {
-                                       $addButton.click();
-                               }
-                       } )
-                       .suggestions( {
-                               fetch: fetchSuggestions,
-                               cancel: function() {
-                                       var req = this.data( 'suggestions-request' );
-                                       // XMLHttpRequest.abort is unimplemented in IE6, also returns nonstandard value of 'unknown' for typeof
-                                       if ( req && typeof req.abort !== 'unknown' && typeof req.abort !== 'undefined' && req.abort ) {
-                                               req.abort();
-                                       }
-                               }
-                       } )
-                       .suggestions();
-
-               $promptContainer
-                       .append( $promptTextbox )
-                       .append( $addButton );
-
-               return $promptContainer;
-       },
-
-       /**
-        * Execute or queue a category addition.
-        *
-        * @param $link {jQuery} Anchor tag of category link inside #catlinks.
-        * @param catTitle {mw.Title} Instance of mw.Title of the category to be added.
-        * @param catSortkey {String} sort key (optional)
-        * @param noAppend
-        * @return {mw.ajaxCategories}
-        */
-       handleCategoryAdd: function( $link, catTitle, catSortkey, noAppend ) {
-               var     ajaxcat = this,
-                       // Suffix is wikitext between '[[Category:Foo' and ']]'.
-                       suffix = catSortkey ? '|' + catSortkey : '',
-                       catName = catTitle.getMainText(),
-                       catFull = catTitle.toText();
-
-               if ( this.containsCat( catName ) ) {
-                       this.showError( mw.msg( 'ajax-category-already-present', catName ) );
-                       return this;
-               }
-
-               if ( !$link.length ) {
-                       $link = this.createCatLink( catTitle );
-               }
-
-               // Mark red if missing
-               $link[(catTitle.exists() === false ? 'addClass' : 'removeClass')]( 'new' );
-
-               this.doConfirmEdit( {
-                       modFn: function( oldText ) {
-                               var newText = ajaxcat.runHooks( oldText, 'beforeAdd', catName );
-                               newText = newText + "\n[[" + catFull + suffix + "]]\n";
-                               return ajaxcat.runHooks( newText, 'afterAdd', catName );
-                       },
-                       dialogDescription: mw.message( 'ajax-add-category-summary', catName ).escaped(),
-                       editSummary: '+[[' + catFull + ']]',
-                       doneFn: function( unsaved ) {
-                               if ( !noAppend ) {
-                                       ajaxcat.options.$container
-                                               .find( '#mw-normal-catlinks > .mw-addcategory-prompt' ).children( 'input' ).hide();
-                                       ajaxcat.options.$container
-                                               .find( '#mw-normal-catlinks ul' ).append( $link.parent() );
-                               } else {
-                                       // Remove input box & button
-                                       $link.data( 'deleteButton' ).click();
-
-                                       // Update link text and href
-                                       $link.show().text( catName ).attr( 'href', catTitle.getUrl() );
-                               }
-                               if ( unsaved ) {
-                                       $link.addClass( 'mw-added-category' );
-                               }
-                               $( '.mw-ajax-addcategory' ).click();
-                       },
-                       $link: $link,
-                       action: 'add'
-               } );
-               return this;
-       },
-
-       /**
-        * Execute or queue a category edit.
-        *
-        * @param $link {jQuery} Anchor tag of category link in #catlinks.
-        * @param oldCatName {String} Name of category before edit
-        * @param catTitle {mw.Title} Instance of mw.Title for new category
-        * @param catSortkey {String} Sort key of new category link (optional)
-        * @param isAdded {Boolean} True if this is a new link, false if it changed an existing one
-        */
-       handleCategoryEdit: function( $link, oldCatName, catTitle, catSortkey, isAdded ) {
-               var     ajaxcat = this,
-                       catName = catTitle.getMainText();
-
-               // Category add needs to be handled differently
-               if ( isAdded ) {
-                       // Pass sortkey back
-                       this.handleCategoryAdd( $link, catTitle, catSortkey, true );
-                       return;
-               }
-
-               // User didn't change anything, trigger delete
-               // @todo Document why it's deleted.
-               if ( oldCatName === catName ) {
-                       $link.data( 'deleteButton' ).click();
-                       return;
-               }
-
-               // Mark red if missing
-               $link[(catTitle.exists() === false ? 'addClass' : 'removeClass')]( 'new' );
-
-               var     categoryRegex = buildRegex( oldCatName ),
-                       editSummary = '[[' + new mw.Title( oldCatName, catNsId ).toText() + ']] -> [[' + catTitle.toText() + ']]';
-
-               ajaxcat.doConfirmEdit({
-                       modFn: function( oldText ) {
-                               var     newText = ajaxcat.runHooks( oldText, 'beforeChange', oldCatName, catName ),
-                                       matches = newText.match( categoryRegex );
-
-                               // Old cat wasn't found, likely to be transcluded
-                               if ( !$.isArray( matches ) ) {
-                                       ajaxcat.showError( mw.msg( 'ajax-edit-category-error' ) );
-                                       return false;
-                               }
-
-                               var     suffix = catSortkey ? '|' + catSortkey : matches[0].replace( categoryRegex, '$2' ),
-                                       newCategoryWikitext = '[[' + catTitle + suffix + ']]';
-
-                               if ( matches.length > 1 ) {
-                                       // The category is duplicated. Remove all but one match
-                                       for ( var i = 1; i < matches.length; i++ ) {
-                                               oldText = oldText.replace( matches[i], '' );
-                                       }
-                               }
-                               newText = oldText.replace( categoryRegex, newCategoryWikitext );
-
-                               return ajaxcat.runHooks( newText, 'afterChange', oldCatName, catName );
-                       },
-                       dialogDescription: mw.message( 'ajax-edit-category-summary', oldCatName, catName ).escaped(),
-                       editSummary: editSummary,
-                       doneFn: function( unsaved ) {
-                               // Remove input box & button
-                               $link.data( 'deleteButton' ).click();
-
-                               // Update link text and href
-                               $link.show().text( catName ).attr( 'href', catTitle.getUrl() );
-                               if ( unsaved ) {
-                                       $link.data( 'origCat', oldCatName ).addClass( 'mw-changed-category' );
-                               }
-                       },
-                       $link: $link,
-                       action: 'edit'
-               });
-       },
-
-       /**
-        * Checks the API whether the category in question is a redirect.
-        * Also returns existance info (to color link red/blue)
-        * @param category {String} Name of category to resolve
-        * @param callback {Function} Called with 1 argument (mw.Title object)
-        */
-       resolveRedirects: function( category, callback ) {
-               if ( !this.options.resolveRedirects ) {
-                       callback( category, true );
-                       return;
-               }
-               var     catTitle = new mw.Title( category, catNsId ),
-                       queryVars = {
-                               action:'query',
-                               titles: catTitle.toString(),
-                               redirects: 1,
-                               format: 'json'
-                       };
-
-               $.getJSON( mw.util.wikiScript( 'api' ), queryVars, function( json ) {
-                       var     redirect = json.query.redirects,
-                               exists = !json.query.pages[-1];
-
-                       // If it's a redirect 'exists' is for the target, not the origin
-                       if ( redirect ) {
-                               // Register existance of redirect origin as well,
-                               // a non-existent page can't be a redirect.
-                               mw.Title.exist.set( catTitle.toString(), true );
-
-                               // Override title with the redirect target
-                               catTitle = new mw.Title( redirect[0].to ).getMainText();
-                       }
-
-                       // Register existence
-                       mw.Title.exist.set( catTitle.toString(), exists );
-
-                       callback( catTitle );
-               } );
-       },
-
-       /**
-        * Append edit and remove buttons to a given category link
-        *
-        * @param DOMElement element Anchor element, to which the buttons should be appended.
-        * @return {mw.ajaxCategories}
-        */
-       createCatButtons: function( $element ) {
-               var     deleteButton = createButton( 'icon-close', mw.msg( 'ajax-remove-category' ) ),
-                       editButton = createButton( 'icon-edit', mw.msg( 'ajax-edit-category' ) ),
-                       saveButton = createButton( 'icon-tick', mw.msg( 'ajax-confirm-save' ) ).hide(),
-                       ajaxcat = this;
-
-               deleteButton.click( this.handleDeleteLink );
-               editButton.click( ajaxcat.createEditInterface );
-
-               $element.after( deleteButton ).after( editButton );
-
-               // Save references to all links and buttons
-               $element.data( {
-                       deleteButton: deleteButton,
-                       editButton: editButton,
-                       saveButton: saveButton
-               } );
-               editButton.data( {
-                       link: $element
-               } );
-               return this;
-       },
-
-       /**
-        * Append spinner wheel to element.
-        * @param $el {jQuery}
-        * @return {mw.ajaxCategories}
-        */
-       addProgressIndicator: function( $el ) {
-               $el.append( $( '<div>' ).addClass( 'mw-ajax-loader' ) );
-               return this;
-       },
-
-       /**
-        * Find and remove spinner wheel from inside element.
-        * @param $el {jQuery}
-        * @return {mw.ajaxCategories}
-        */
-       removeProgressIndicator: function( $el ) {
-               $el.find( '.mw-ajax-loader' ).remove();
-               return this;
-       },
-
-       /**
-        * Parse the DOM $container and build a list of
-        * present categories.
-        *
-        * @return {Array} All categories.
-        */
-       getCats: function() {
-               var cats = this.options.$container
-                               .find( this.options.categoryLinkSelector )
-                               .map( function() {
-                                       return $.trim( $( this ).text() );
-                               } );
-               return cats;
-       },
-
-       /**
-        * Check whether a passed category is present in the DOM.
-        *
-        * @param newCat {String} Category name to be checked for.
-        * @return {Boolean}
-        */
-       containsCat: function( newCat ) {
-               newCat = $.ucFirst( newCat );
-               var match = false;
-               $.each( this.getCats(), function(i, cat) {
-                       if ( $.ucFirst( cat ) === newCat ) {
-                               match = true;
-                               // Stop once we have a match
-                               return false;
-                       }
-               } );
-               return match;
-       },
-
-       /**
-        * Execute or queue a category delete.
-        *
-        * @param $link {jQuery}
-        * @param category
-        * @return ?
-        */
-       handleCategoryDelete: function( $link, category ) {
-               var     categoryRegex = buildRegex( category, true ),
-                       ajaxcat = this;
-
-               this.doConfirmEdit({
-                       modFn: function( oldText ) {
-                               var newText = ajaxcat.runHooks( oldText, 'beforeDelete', category );
-                               newText = newText.replace( categoryRegex, '' );
-
-                               if ( newText === oldText ) {
-                                       ajaxcat.showError( mw.msg( 'ajax-remove-category-error' ) );
-                                       return false;
-                               }
-
-                               return ajaxcat.runHooks( newText, 'afterDelete', category );
-                       },
-                       dialogDescription: mw.message( 'ajax-remove-category-summary', category ).escaped(),
-                       editSummary: '-[[' + new mw.Title( category, catNsId ) + ']]',
-                       doneFn: function( unsaved ) {
-                               if ( unsaved ) {
-                                       $link.addClass( 'mw-removed-category' );
-                               } else {
-                                       $link.parent().remove();
-                               }
-                       },
-                       $link: $link,
-                       action: 'delete'
-               });
-       },
-
-       /**
-        * Takes a category link element
-        * and strips all data from it.
-        *
-        * @param $link {jQuery}
-        * @param del {Boolean}
-        * @param dontRestoreText {Boolean}
-        * @return ?
-        */
-       resetCatLink: function( $link, del, dontRestoreText ) {
-               $link.removeClass( 'mw-removed-category mw-added-category mw-changed-category' );
-               var data = $link.data();
-
-               if ( typeof data.stashIndex === 'number' ) {
-                       this.removeStashItem( data.stashIndex );
-               }
-               if ( del ) {
-                       $link.parent().remove();
-                       return;
-               }
-               if ( data.origCat && !dontRestoreText ) {
-                       var catTitle = new mw.Title( data.origCat, catNsId );
-                       $link.text( catTitle.getMainText() );
-                       $link.attr( 'href', catTitle.getUrl() );
-               }
-
-               $link.removeData();
-
-               // Re-add data
-               $link.data( {
-                       saveButton: data.saveButton,
-                       deleteButton: data.deleteButton,
-                       editButton: data.editButton
-               } );
-       },
-
-       /**
-        * Do the actual edit.
-        * Gets token & text from api, runs it through fn
-        * and saves it with summary.
-        * @param page {String} Pagename
-        * @param fn {Function} edit function
-        * @param summary {String}
-        * @param doneFn {String} Callback after all is done
-        */
-       doEdit: function( page, fn, summary, doneFn ) {
-               // Get an edit token for the page.
-               var getTokenVars = {
-                       action: 'query',
-                       prop: 'info|revisions',
-                       intoken: 'edit',
-                       titles: page,
-                       rvprop: 'content|timestamp',
-                       format: 'json'
-               }, ajaxcat = this;
-
-               $.post(
-                       mw.util.wikiScript( 'api' ),
-                       getTokenVars,
-                       function( json ) {
-                               if ( 'error' in json ) {
-                                       ajaxcat.showError( mw.msg( 'ajax-api-error', json.error.code, json.error.info ) );
-                                       return;
-                               } else if ( json.query && json.query.pages ) {
-                                       var infos = json.query.pages;
-                               } else {
-                                       ajaxcat.showError( mw.msg( 'ajax-api-unknown-error' ) );
-                                       return;
-                               }
-
-                               $.each( infos, function( pageid, data ) {
-                                       var     token = data.edittoken,
-                                               timestamp = data.revisions[0].timestamp,
-                                               oldText = data.revisions[0]['*'],
-                                               nowikiKey = generateRandomId(), // Unique ID for nowiki replacement
-                                               nowikiFragments = []; // Nowiki fragments will be stored here during the changes
-
-                                       // Replace all nowiki parts with unique keys..
-                                       oldText = replaceNowikis( oldText, nowikiKey, nowikiFragments );
-
-                                       // ..then apply the changes to the page text..
-                                       var newText = fn( oldText );
-                                       if ( newText === false ) {
-                                               return;
-                                       }
-
-                                       // ..and restore the nowiki parts back.
-                                       newText = restoreNowikis( newText, nowikiKey, nowikiFragments );
-
-                                       var postEditVars = {
-                                               action: 'edit',
-                                               title: page,
-                                               text: newText,
-                                               summary: summary,
-                                               token: token,
-                                               basetimestamp: timestamp,
-                                               format: 'json'
-                                       };
-
-                                       $.post(
-                                               mw.util.wikiScript( 'api' ),
-                                               postEditVars,
-                                               doneFn,
-                                               'json'
-                                       )
-                                       .error( function( xhr, text, error ) {
-                                               ajaxcat.showError( mw.msg( 'ajax-api-error', text, error ) );
-                                       });
-                               } );
-                       },
-                       'json'
-               ).error( function( xhr, text, error ) {
-                       ajaxcat.showError( mw.msg( 'ajax-api-error', text, error ) );
-               } );
-       },
-
-       /**
-        * This gets called by all action buttons
-        * Displays a dialog to confirm the action
-        * Afterwards do the actual edit.
-        *
-        * @param props {Object}:
-        * - modFn {Function} text-modifying function
-        * - dialogDescription {String} Changes done (HTML for in the dialog, escape before hand if needed)
-        * - editSummary {String} Changes done (text for the edit summary)
-        * - doneFn {Function} callback after everything is done
-        * - $link {jQuery}
-        * - action
-        * @return {mw.ajaxCategories}
-        */
-       doConfirmEdit: function( props ) {
-               var summaryHolder, reasonBox, dialog, submitFunction,
-                       buttons = {},
-                       dialogOptions = {
-                               AutoOpen: true,
-                               buttons: buttons,
-                               width: 450
-                       },
-                       ajaxcat = this;
-
-               // Check whether to use multiEdit mode:
-               if ( this.options.multiEdit && props.action !== 'all' ) {
-
-                       // Stash away
-                       props.$link
-                               .data( 'stashIndex', this.stash.fns.length )
-                               .data( 'summary', props.dialogDescription );
-
-                       this.stash.dialogDescriptions.push( props.dialogDescription );
-                       this.stash.editSummaries.push( props.editSummary );
-                       this.stash.fns.push( props.modFn );
-
-                       this.saveAllButton.show();
-                       this.cancelAllButton.show();
-
-                       // Clear input field after action
-                       ajaxcat.addContainer.find( '.mw-addcategory-input' ).val( '' );
-
-                       // This only does visual changes, fire done and return.
-                       props.doneFn( true );
-                       return this;
-               }
-
-               // Summary of the action to be taken
-               summaryHolder = $( '<p>' )
-                       .html( '<strong>' + mw.message( 'ajax-category-question' ).escaped() + '</strong><br/>' + props.dialogDescription );
-
-               // Reason textbox.
-               reasonBox = $( '<input type="text" size="45"></input>' )
-                       .addClass( 'mw-ajax-confirm-reason' );
-
-               // Produce a confirmation dialog
-               dialog = $( '<div>' )
-                       .addClass( 'mw-ajax-confirm-dialog' )
-                       .attr( 'title', mw.msg( 'ajax-confirm-title' ) )
-                       .append( summaryHolder )
-                       .append( reasonBox );
-
-               // Submit button
-               submitFunction = function() {
-                       ajaxcat.addProgressIndicator( dialog );
-                       ajaxcat.doEdit(
-                               mw.config.get( 'wgPageName' ),
-                               props.modFn,
-                               props.editSummary + ': ' + reasonBox.val(),
-                               function() {
-                                       props.doneFn();
-
-                                       // Clear input field after successful edit
-                                       ajaxcat.addContainer.find( '.mw-addcategory-input' ).val( '' );
-
-                                       dialog.dialog( 'close' );
-                                       ajaxcat.removeProgressIndicator( dialog );
-                               }
-                       );
-               };
-
-               buttons[mw.msg( 'ajax-confirm-save' )] = submitFunction;
-
-               dialog.dialog( dialogOptions ).keyup( function( e ) {
-                       // Close on enter
-                       if ( e.keyCode === 13 ) {
-                               submitFunction();
-                       }
-               } );
-
-               return this;
-       },
-
-       /**
-        * @param index {Number|jQuery} Stash index or jQuery object of stash item.
-        * @return {mw.ajaxCategories}
-        */
-       removeStashItem: function( i ) {
-               if ( typeof i !== 'number' ) {
-                       i = i.data( 'stashIndex' );
-               }
-
-               try {
-                       delete this.stash.fns[i];
-                       delete this.stash.dialogDescriptions[i];
-               } catch(e) {}
-
-               if ( $.isEmpty( this.stash.fns ) ) {
-                       this.stash.fns = [];
-                       this.stash.dialogDescriptions = [];
-                       this.stash.editSummaries = [];
-                       this.saveAllButton.hide();
-                       this.cancelAllButton.hide();
-               }
-               return this;
-       },
-
-       /**
-        * Reset all data from the category links and the stash.
-        *
-        * @param del {Boolean} Delete any category links with .mw-removed-category
-        * @return {mw.ajaxCategories}
-        */
-       resetAll: function( del ) {
-               var     $links = this.options.$container.find( this.options.categoryLinkSelector ),
-                       $del = $([]),
-                       ajaxcat = this;
-
-               if ( del ) {
-                       $del = $links.filter( '.mw-removed-category' ).parent();
-               }
-
-               $links.each( function() {
-                       ajaxcat.resetCatLink( $( this ), false, del );
-               } );
-
-               $del.remove();
-
-               this.options.$container.find( '#mw-hidden-catlinks' ).remove();
-
-               return this;
-       },
-
-       /**
-        * Add hooks
-        * Currently available: beforeAdd, beforeChange, beforeDelete,
-        *                                              afterAdd, afterChange, afterDelete
-        * If the hook function returns false, all changes are aborted.
-        *
-        * @param string type Type of hook to add
-        * @param function fn Hook function. The following vars are passed to it:
-        *      1. oldtext: The wikitext before the hook
-        *      2. category: The deleted, added, or changed category
-        *      3. (only for beforeChange/afterChange): newcategory
-        */
-       addHook: function( type, fn ) {
-               if ( !this.hooks[type] || !$.isFunction( fn ) ) {
-                       return;
-               }
-               else {
-                       this.hooks[type].push( fn );
-               }
-       },
-
-
-       /**
-        * Open a dismissable error dialog
-        *
-        * @param string str The error description
-        */
-       showError: function( str ) {
-               var     oldDialog = $( '.mw-ajax-confirm-dialog' ),
-                       buttons = {},
-                       dialogOptions = {
-                               buttons: buttons,
-                               AutoOpen: true,
-                               title: mw.msg( 'ajax-error-title' )
-                       };
-
-               this.removeProgressIndicator( oldDialog );
-               oldDialog.dialog( 'close' );
-
-               var dialog = $( '<div>' ).text( str );
-
-               mw.util.$content.append( dialog );
-
-               buttons[mw.msg( 'ajax-confirm-ok' )] = function( e ) {
-                       dialog.dialog( 'close' );
-               };
-
-               dialog.dialog( dialogOptions ).keyup( function( e ) {
-                       if ( e.keyCode === 13 ) {
-                               dialog.dialog( 'close' );
-                       }
-               } );
-       },
-
-       /**
-        * @param oldtext
-        * @param type
-        * @param category
-        * @param categoryNew
-        * @return oldtext
-        */
-       runHooks: function( oldtext, type, category, categoryNew ) {
-               // No hooks registered
-               if ( !this.hooks[type] ) {
-                       return oldtext;
-               } else {
-                       for ( var i = 0; i < this.hooks[type].length; i++ ) {
-                               oldtext = this.hooks[type][i]( oldtext, category, categoryNew );
-                               if ( oldtext === false ) {
-                                       this.showError( mw.msg( 'ajax-category-hook-error', category ) );
-                                       return;
-                               }
-                       }
-                       return oldtext;
-               }
-       }
-};
-
-} )( jQuery );