From: Krinkle Date: Sun, 4 Sep 2011 19:35:22 +0000 (+0000) Subject: Split ajaxCategories away from core for now, into an extension: X-Git-Tag: 1.31.0-rc.0~27916 X-Git-Url: http://git.cyclocoop.org/%22.%24h.%22?a=commitdiff_plain;h=d0677222a5f06276615e6b9192ad1972291b17bf;p=lhc%2Fweb%2Fwiklou.git 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. --- diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 972e4666fb..23cdb6e8d7 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -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 diff --git a/includes/OutputPage.php b/includes/OutputPage.php index a45a3ad547..e81ec923b2 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -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' ); - } - } } /** diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index 66cf503f83..6755f6cb12 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -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.', - ); diff --git a/languages/messages/MessagesQqq.php b/languages/messages/MessagesQqq.php index c95ee2a2c9..6e825573ec 100644 --- a/languages/messages/MessagesQqq.php +++ b/languages/messages/MessagesQqq.php @@ -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".', - ); diff --git a/maintenance/language/messages.inc b/maintenance/language/messages.inc index 755cfc13cb..e6e6795689 100644 --- a/maintenance/language/messages.inc +++ b/maintenance/language/messages.inc @@ -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', ); diff --git a/resources/Resources.php b/resources/Resources.php index 6613cb3559..b5ceed1c00 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -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 index 9815f4fbcd..0000000000 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 index 2c39d3b951..0000000000 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 index 679459ee40..0000000000 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 index 2aaef6c150..0000000000 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 index afd03d9f76..0000000000 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 index 62592a2dd6..0000000000 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 index 30f9c7a0a4..0000000000 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 index 580450e25b..0000000000 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 index a1e0220d07..0000000000 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 index 2e7bd36332..0000000000 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 index bf1b95d4a5..0000000000 --- a/resources/mediawiki.page/mediawiki.page.ajaxCategories.css +++ /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 index a90342f8b5..0000000000 --- a/resources/mediawiki.page/mediawiki.page.ajaxCategories.init.js +++ /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 index 916ec6b4a6..0000000000 --- a/resources/mediawiki.page/mediawiki.page.ajaxCategories.js +++ /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: '
  • ', - $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 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( /([\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 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 = $( '' ) - .addClass( className || '' ) - .attr( 'title', title ) - .html( '​' ); - - if ( text ) { - var $icon = $( '' ).addClass( 'icon ' + icon ).html( '​' ); - $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( '
    ' ); - } - - // 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 = $( '
    ' ) - .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 = $( '
    ' ), - $promptTextbox = $( '' ), - $addButton = $( '' ), - 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( $( '
    ' ).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 = $( '

    ' ) - .html( '' + mw.message( 'ajax-category-question' ).escaped() + '
    ' + props.dialogDescription ); - - // Reason textbox. - reasonBox = $( '' ) - .addClass( 'mw-ajax-confirm-reason' ); - - // Produce a confirmation dialog - dialog = $( '

    ' ) - .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 = $( '
    ' ).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 );