From 62a3e10fea063972e181e494979eb18a3497c0b2 Mon Sep 17 00:00:00 2001 From: Krinkle Date: Thu, 28 Jul 2011 00:43:21 +0000 Subject: [PATCH] AjaxCategories rewrite: Solving syntax problems, performance improvements and applying code conventions: * Replaced sprite image with separate images and letting ResourceLoader embed them with @embed (@embed means 0 http requests, less maintenance, none of the known limitations with sprites, and more readable code (named files rather than pixel offsets) * Many functions were floating in the global namespace (like window.makeCaseInsensitive). A statement ends after a semi-colon(;). All functions declared after "catUrl" were assigned to the window object. I've instead turned the semi-colons back into comma's, merged some other var statements and moved them to the top of the closure. Changed local function declarations into function expressions for clarity. * fetchSuggestions is called by $.fn.suggestions like ".call( $textbox, $textbox.val() )". So the context (this) isn't the raw element but the jQuery object, no need to re-construct with "$(this)" or "$(that)" which is slow and shouldn't even work. jQuery methods can be called on it directly. I've also replaced "$(this).val()" with the value-argument passed to fetchSuggestions which has this exact value already. * Adding more function documentation. And changing @since to 1.19 as this was merged from js2-branch into 1.19-trunk and new features aren't backported to 1.18. * Optimizing options/default construction to just "options = $.extend( {}, options )". Caching defaultOptions is cool, but doesn't really work if it's in a context/instance local variable. Moved it up to the module closure var statements, now it's static across all instances. * In makeSuggestionBox(): Fixing invalid html fragments passed to jQuery that fail in IE. Shortcuts (like '' and '') are only allowed for createElement triggers, not when creating longer fragments with content and/or attributes which are created through innerHTML, in the latter case the HTML must be completely valid and is not auto-corrected by IE. * Using more jQuery chaining where possible. * In buildRegex(): Using $.map with join( '|' ), (rather than $.each with += '|'; and substr). * Storing the init instance of mw.ajaxCategories in mw.page for reference (rather than local/anonymous). * Applied some best practices and "write testable code" ** Moved some of the functions created on the fly and assigned to 'this' into prototype (reference is cheaper) ** Making sure at least all 'do', 'set' and/or 'prototype' functions have a return value. Even if it's just a simple boolean true or context/this for chain-ability. ** Rewrote confirmEdit( .., .., .., ) as a prototyped method named "doConfirmEdit" which takes a single props-object with named valuas as argument, instead of list with 8 arguments. * Removed trailing whitespace and other minor fixes to comply with the code conventions. ** Removed space between function name and caller: "foo ()" => foo()) ** Changing "someArray.indexOf() + 1" into "someArr.indexOf() !== -1". We want a Boolean here, not a Number. ** Renamed all underscore-variables to non-underscore variants. == Bug fixes == * When adding a category that is not already on the page as-is but of which the clean() version is already on the page, the script would fail. Fixed it by moving the checks up in handleCategoryAdd() and making sure that createCatLink() actually returned something. * confirmEdit() wasn't working properly and had unused code (such as submitButton), removed hidden prepending to #catlinks, no need to, it can be dialog'ed directly from the jQuery object without being somewhere in the document. * in doConfirmEdit() in submitFunction() and multiEdit: Clearing the input field after adding a category, so that when another category is being added it doesn't start with the previous value which is not allowed to be added again... --- .../images/AJAXCategorySprite.png | Bin 384 -> 0 bytes .../images/ajaxcat-add-hover.png | Bin 0 -> 156 bytes .../mediawiki.page/images/ajaxcat-add.png | Bin 0 -> 156 bytes .../images/ajaxcat-close-hover.png | Bin 0 -> 162 bytes .../mediawiki.page/images/ajaxcat-close.png | Bin 0 -> 162 bytes .../images/ajaxcat-edit-hover.png | Bin 0 -> 184 bytes .../mediawiki.page/images/ajaxcat-edit.png | Bin 0 -> 184 bytes .../images/ajaxcat-error-hover.png | Bin 0 -> 184 bytes .../mediawiki.page/images/ajaxcat-error.png | Bin 0 -> 184 bytes .../images/ajaxcat-tick-hover.png | Bin 0 -> 184 bytes .../mediawiki.page/images/ajaxcat-tick.png | Bin 0 -> 184 bytes .../mediawiki.page.ajaxCategories.css | 31 +- .../mediawiki.page.ajaxCategories.init.js | 7 +- .../mediawiki.page.ajaxCategories.js | 1611 +++++++++-------- .../mediawiki.page/mediawiki.page.startup.js | 2 + 15 files changed, 907 insertions(+), 744 deletions(-) delete mode 100644 resources/mediawiki.page/images/AJAXCategorySprite.png create mode 100644 resources/mediawiki.page/images/ajaxcat-add-hover.png create mode 100644 resources/mediawiki.page/images/ajaxcat-add.png create mode 100644 resources/mediawiki.page/images/ajaxcat-close-hover.png create mode 100644 resources/mediawiki.page/images/ajaxcat-close.png create mode 100644 resources/mediawiki.page/images/ajaxcat-edit-hover.png create mode 100644 resources/mediawiki.page/images/ajaxcat-edit.png create mode 100644 resources/mediawiki.page/images/ajaxcat-error-hover.png create mode 100644 resources/mediawiki.page/images/ajaxcat-error.png create mode 100644 resources/mediawiki.page/images/ajaxcat-tick-hover.png create mode 100644 resources/mediawiki.page/images/ajaxcat-tick.png diff --git a/resources/mediawiki.page/images/AJAXCategorySprite.png b/resources/mediawiki.page/images/AJAXCategorySprite.png deleted file mode 100644 index d5f9cf48efc278326f3fab552a7ff3f266f3f378..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 384 zcmV-`0e}99P)OCbON0Z=j94X$Y?xp!!^Q($@K3geXUQ&xUqWcg)?_2cZNs5PFfu0f; zJV|*k-ZZbs(Wd^ z*8Hj>p3hc_mzUJyjp#lE(EAp>c#`6wSD>ea1y54mi#N?K?A>v#c%gRT2^xl@Ln>}1{rUgjo>{e_v(d@m1KWlL+!kj-9iJ(&3n=## zD*QZ`@QrEKX3nbgf~v-e6E?GlJ0;2dn#rkffZ_KNUitqrE2aQVV(@hJb6Mw<&;$VD C{4^5) literal 0 HcmV?d00001 diff --git a/resources/mediawiki.page/images/ajaxcat-add.png b/resources/mediawiki.page/images/ajaxcat-add.png new file mode 100644 index 0000000000000000000000000000000000000000..2c39d3b951a156597342798b7d3602a9c3aa848b GIT binary patch literal 156 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5X>^xl@Ln>}1{rUgjo>{e_v(d@m1KWlL+&@I46^kb_cL*N4 zaiu!PpqlaQO_sgJ7JCnPoVeMnt!T*i_Y8|b0>iH}yz)=31TzCoV(@hJb6Mw<&;$Sj C=QMf% literal 0 HcmV?d00001 diff --git a/resources/mediawiki.page/images/ajaxcat-close-hover.png b/resources/mediawiki.page/images/ajaxcat-close-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..679459ee401bae914be239d75200a9b2413164cb GIT binary patch literal 162 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5XTs&PILn>}1{rUgjo>{e_v(d?bhbQ5nT7~CCX1PsO-|Gz) zefBJ3+?4jKm2sEx$4=vkk8*MzZRvN5x!1^e?BbpS3<3!ZHrcf literal 0 HcmV?d00001 diff --git a/resources/mediawiki.page/images/ajaxcat-close.png b/resources/mediawiki.page/images/ajaxcat-close.png new file mode 100644 index 0000000000000000000000000000000000000000..2aaef6c150d9ab6549064b634155b6e056a53ad9 GIT binary patch literal 162 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5XTs&PILn>}1{rUgjo>{e_v(d?bhbQ5n+Kziy`Q$b=?&ddK z^x3nBaZ}o_R>ob%A3KdFKFY~?w58uI=3XP?v5R{SFbE_tupgF^-p;(?7tlNgPgg&e IbxsLQ0D^EheE}1{rUgjo>{e_v(d@m1KS3H)d8iyqAL>ao)bw^ zImvormghGn593X}Dv}JFC2tCp9F{0t%A9lV*fSwDFOy``29dKatV(ugS$GweNaic> f&E3NwkianEj8^{Re>_WpwlH|Q`njxgN@xNANH0B` literal 0 HcmV?d00001 diff --git a/resources/mediawiki.page/images/ajaxcat-edit.png b/resources/mediawiki.page/images/ajaxcat-edit.png new file mode 100644 index 0000000000000000000000000000000000000000..62592a2dd6b8b040f215d8cf3b4f11819c89a587 GIT binary patch literal 184 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5XqC8z3Ln>}1{rUgjo>{e_v(d@m1KS3H)f-BFMph)0x=R^P zdcu@)R;8M8iR4YUNjwcVdvZ8-CHCyN#AueDT+At~I%}hpLs!}&Cc*h>O>6>}ddvmd e{o)un3>YrNXzAy~$L|2z!r}1{rUgjo>{e_v(d@m1KS3LVm-~G1I#Y(|M9yy zJ*tRhxZJX4W{SSRrJh8FU7s&7npLVLi98feSGsm{$B~&HAB}+mL4SR=FdCd~;YpPF eHIq}}0K=F4TGHo>rUV0RVeoYIb6Mw<&;$UvXF(SL literal 0 HcmV?d00001 diff --git a/resources/mediawiki.page/images/ajaxcat-error.png b/resources/mediawiki.page/images/ajaxcat-error.png new file mode 100644 index 0000000000000000000000000000000000000000..580450e25b6be0600ed775a85fcdf19caf7d034b GIT binary patch literal 184 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5XqC8z3Ln>}1{rUgjo>{e_v(d@m1KS3L;%mEa9b{H1_^#;Y z^r#}1;d0BGnJM}LmwFNzc749UXjZ9~B=S%=UFq7*9Y}1{rUgjo>{e_v(d@m2v5O`)t~0)Fel8q$eXBi zJT1jeShFkffx{4}FtS3)M6G&)LRmth%%ycVcH1l+tWw@uoThQ+COb$)8Cq~B> e&Y#J^#KVx%VJNDvsyi8I3xlVtpUXO@geCy}b31MT literal 0 HcmV?d00001 diff --git a/resources/mediawiki.page/images/ajaxcat-tick.png b/resources/mediawiki.page/images/ajaxcat-tick.png new file mode 100644 index 0000000000000000000000000000000000000000..2e7bd363323797cfa43d918328403e5f30963645 GIT binary patch literal 184 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5XqC8z3Ln>}1{rUgjo>{e_v(d@m2+s%YZNL1tF&dnG(Pl6) z+1O})r^r!*4J*G(D2Fj$nCt0b%wcdyNXYDHi^<|046{`f&q~A{SjsW~p-+p5aFOKW f3+H_p7}*%4xePf~+z-qE+QQ)J>gTe~DWM4fWr#f_ literal 0 HcmV?d00001 diff --git a/resources/mediawiki.page/mediawiki.page.ajaxCategories.css b/resources/mediawiki.page/mediawiki.page.ajaxCategories.css index 2e62f8c965..11ded38c53 100644 --- a/resources/mediawiki.page/mediawiki.page.ajaxCategories.css +++ b/resources/mediawiki.page/mediawiki.page.ajaxCategories.css @@ -9,30 +9,34 @@ .mw-remove-category { padding: 2px 8px; - display:inline; + 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; + display: inline-block; } + #catlinks .icon { cursor: pointer; padding: 1px 8px; margin: 0; - background: url('images/AJAXCategorySprite.png') 0 0 no-repeat; + background-position: 0 0 ; + background-repeat: no-repeat; opacity: 0.5; } @@ -40,32 +44,27 @@ cursor: pointer; margin-right: 1em; } -#catlinks .icon-parent:hover .icon { - background-position-y: -16px; -} -#catlinks .no-text { -} #catlinks .icon-close { - background-position: 0 0; + /* @embed */ background-image: url(images/ajaxcat-close.png); } #catlinks .icon-edit { - background-position: -16px 0; + /* @embed */ background-image: url(images/ajaxcat-edit.png); } #catlinks .icon-tick { - background-position: -32px 0; + /* @embed */ background-image: url(images/ajaxcat-tick.png); } #catlinks .icon-add { - background-position: -64px 0; + /* @embed */ background-image: url(images/ajaxcat-add.png); } #catlinks .icon-close:hover { - background-position: 0 -16px; + /* @embed */ background-image: url(images/ajaxcat-close-hover.png); } #catlinks .icon-edit:hover { - background-position: -16px -16px; + /* @embed */ background-image: url(images/ajaxcat-edit-hover.png); } #catlinks .icon-tick:hover { - background-position: -32px -16px; + /* @embed */ background-image: url(images/ajaxcat-tick-hover.png); } #catlinks .icon-add:hover { - background-position: -64px -16px; + /* @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 index c1f5df1977..a82cf3ffb1 100644 --- a/resources/mediawiki.page/mediawiki.page.ajaxCategories.init.js +++ b/resources/mediawiki.page/mediawiki.page.ajaxCategories.init.js @@ -1 +1,6 @@ -jQuery( document ).ready( new mw.ajaxCategories().setup ); +mw.page.ajaxCategories = new mw.ajaxCategories(); +jQuery( document ).ready( function(){ + // Seperate 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 index 2429115dbc..ca340511e4 100644 --- a/resources/mediawiki.page/mediawiki.page.ajaxCategories.js +++ b/resources/mediawiki.page/mediawiki.page.ajaxCategories.js @@ -3,704 +3,654 @@ * * @author Michael Dale, 2009 * @author Leo Koppelkamm, 2011 - * @since 1.18 + * @author Timo Tijhof, 2011 + * @since 1.19 * - * Relies on: mw.config (wgFormattedNamespaces, wgNamespaceIds, wgCaseSensitiveNamespaces, wgUserGroups), - * mw.util.wikiGetlink, mw.user.getId + * 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 + }; - clean = function( s ) { + function clean( s ) { if ( s !== undefined ) { return s.replace( /[\x00-\x1f\x23\x3c\x3e\x5b\x5d\x7b\x7c\x7d\x7f\s]+/g, '' ); } - }, + } /** * Build URL for passed Category - * - * @param string category name. - * @return string Valid URL + * + * @param cat {String} Category name. + * @return {String} Valid URL */ - catUrl = function( cat ) { + function catUrl( cat ) { return mw.util.wikiGetlink( new mw.Title( cat, catNsId ) ); - }; + } /** - * Helper function for $.fn.suggestion - * - * @param string Query string. + * Helper function for $.fn.suggestions + * + * @context {jQuery} + * @param value {String} Textbox value. */ - fetchSuggestions = function( query ) { - var _this = this; - // ignore bad characters, they will be stripped out - var catName = clean( $( this ).val() ); - var request = $.ajax( { + 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' + 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; - var titleArr = []; + var title, + pages = data.query.allpages, + titleArr = []; $.each( pages, function( i, page ) { - var title = page.title.split( ':', 2 )[1]; + title = page.title.split( ':', 2 )[1]; titleArr.push( title ); } ); - $( _this ).suggestions( 'suggestions', titleArr ); + $el.suggestions( 'suggestions', titleArr ); } } ); - _request = request; - }; - + $el.data( 'suggestions-request', request ); + } + /** - * Replace and comments with unique keys - */ - replaceNowikis = function( text, id, array ) { + * Replace and comments with unique keys + * + * @param text {String} + * @param id + * @param keys {Array} + * @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++ ) { - array[i] = matches[i]; + keys[i] = matches[i]; text = text.replace( matches[i], id + i + '-' ); } return text; - }; - + } + /** - * Restore and comments from unique keys - */ - restoreNowikis = function( text, id, array ) { - for ( var i = 0; i < array.length; i++ ) { - text = text.replace( id + i + '-', array[i] ); + * Restore and comments from unique keys + * @param text {String} + * @param id + * @param keys {Array} + * @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 Regex string. - * @return string Processed regex string + * + * @param string {String} Regex string + * @return {String} Processed regex string */ - makeCaseInsensitive = function( string ) { - if ( $.inArray( 14, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) + 1 ) { + function makeCaseInsensitive( string ) { + var newString = ''; + if ( $.inArray( 14, mw.config.get( 'wgCaseSensitiveNamespaces' ) ) !== -1 ) { return string; } - var newString = ''; - for ( var i=0; i < string.length; i++ ) { + 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 string category. - * @param boolean Match one following linebreak as well? - * @return Regex - */ - buildRegex = function( category, matchLineBreak ) { - var categoryNSFragment = ''; - $.each( mw.config.get( 'wgNamespaceIds' ), function( name, id ) { - if ( id == 14 ) { - // The parser accepts stuff like cATegORy, - // we need to do the same - // ( Well unless we have wgCaseSensitiveNamespaces, but that's being checked for ) - categoryNSFragment += '|' + makeCaseInsensitive ( $.escapeRE( name ) ); + * 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 ) { + return makeCaseInsensitive( $.escapeRE( name ) ); } - } ); - categoryNSFragment = categoryNSFragment.substr( 1 ); // Remove leading pipe + } ).join( '|' ); - // Build the regex - var titleFragment = $.escapeRE( category ).replace( /( |_)/g, '[ _]' ); - - firstChar = titleFragment.charAt( 0 ); firstChar = '[' + firstChar.toUpperCase() + firstChar.toLowerCase() + ']'; titleFragment = firstChar + titleFragment.substr( 1 ); - var categoryRegex = '\\[\\[(' + categoryNSFragment + '):' + '[ _]*' +titleFragment + '(\\|[^\\]]*)?\\]\\]'; + categoryRegex = '\\[\\[(' + categoryNSFragment + '):' + '[ _]*' + titleFragment + '(\\|[^\\]]*)?\\]\\]'; if ( matchLineBreak ) { categoryRegex += '[ \\t\\r]*\\n?'; } return new RegExp( categoryRegex, 'g' ); - }; - - -mw.ajaxCategories = function( options ) { - //Save scope in shortcut - var that = this, _request, _saveAllButton, _cancelAllButton, _addContainer, defaults; - - defaults = { - catLinkWrapper : '
  • ', - $container : $( '.catlinks' ), - $containerNormal : $( '#mw-normal-catlinks' ), - categoryLinkSelector : 'li a:not(.icon)', - multiEdit : $.inArray( 'user', mw.config.get( 'wgUserGroups' ) ) + 1, - resolveRedirects : true - }; - // merge defaults and options, without modifying defaults */ - options = $.extend( {}, defaults, options ); + } /** - * Insert a newly added category into the DOM - * - * @param string category name. - * @return jQuery object + * 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 of button. + * @return {jQuery} The button. */ - this.createCatLink = function( cat ) { - // User can implicitely state a sort key. - // Remove before display - cat = cat.replace(/\|.*/, '' ); - - // strip out bad characters - cat = clean ( cat ); + 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 ( $.isEmpty( cat ) || that.containsCat( cat ) ) { - return; + if ( text ) { + var $icon = $( '' ).addClass( 'icon ' + icon ).html( '​' ); + $button.addClass( 'icon-parent' ).append( $icon ).append( text ); + } else { + $button.addClass( 'icon ' + icon ); } + return $button; + } - var $catLinkWrapper = $( options.catLinkWrapper ); - var $anchor = $( '' ).append( cat ); - $catLinkWrapper.append( $anchor ); - $anchor.attr( { target: "_blank", href: catUrl( cat ) } ); - - _createCatButtons( $anchor ); +/** + * @constructor + * @param + */ +mw.ajaxCategories = function( options ) { - return $anchor; - }; + this.options = options = $.extend( defaultOptions, options ); - /** - * Takes a category link element - * and strips all data from it. - * - * @param jQuery object - */ - this.resetCatLink = function( $link, del, dontRestoreText ) { - $link.removeClass( 'mw-removed-category mw-added-category mw-changed-category' ); - var data = $link.data(); + // Save scope in shortcut + var that = this; - if ( typeof data.stashIndex == "number" ) { - _removeStashItem( data.stashIndex ); - } - if ( del ) { - $link.parent.remove(); - return; - } - if ( data.origCat && !dontRestoreText ) { - $link.text( data.origCat ); - $link.attr( 'href', catUrl( data.origCat ) ); - } + // Elements tied to this instance + this.saveAllButton = null; + this.cancelAllButton = null; + this.addContainer = null; - $link.removeData(); + this.request = null; - //Readd static. - $link.data({ - saveButton : data.saveButton, - deleteButton: data.deleteButton, - editButton : data.editButton - }); + // Stash and hooks + this.stash = { + summaries: [], + shortSum: [], + fns: [] + }; + this.hooks = { + beforeAdd: [], + beforeChange: [], + beforeDelete: [], + afterAdd: [], + afterChange: [], + afterDelete: [] }; + /* Event handlers */ + /** - * Reset all data from the category links and the stash. - * @param Boolean del Delete any category links with .mw-removed-category + * Handle add category submit. Not to be called directly. + * + * @context Element + * @param e {jQuery Event} */ - this.resetAll = function( del ) { - var $links = options.$container.find( options.categoryLinkSelector ), $del = $(); - if ( del ) { - $del = $links.filter( '.mw-removed-category' ).parent(); - } - - $links.each( function() { - that.resetCatLink( $( this ), false, del ); - }); - - $del.remove(); + this.handleAddLink = function( e ) { + var $el = $( this ), + $link = $([]), + categoryText = $.ucFirst( $el.parent().find( '.mw-addcategory-input' ).val() || '' ); - if ( !options.$container.find( '#mw-hidden-catlinks li' ).length ) { - options.$container.find( '#mw-hidden-catlinks' ).remove(); - } + // Resolve redirects + that.resolveRedirects( categoryText, function( resolvedCat, exists ) { + that.handleCategoryAdd( $link, resolvedCat, false, exists ); + } ); }; - + /** - * Create a suggestion box for use in edit/add dialogs - * @param str prefill Prefill input - * @param function callback on submit - * @param str buttonVal Button text + * @context Element + * @param e {jQuery Event} */ - this._makeSuggestionBox = function( prefill, callback, buttonVal ) { - // Create add category prompt - var promptContainer = $( '
    ' ); - var promptTextbox = $( '' ); - if ( prefill !== '' ) { - promptTextbox.val( prefill ); - } - var addButton = $( '' ); - addButton.val( buttonVal ); + this.createEditInterface = function( e ) { + var $el = $( this ), + $link = $el.data( 'link' ), + category = $link.text(), + $input = that.makeSuggestionBox( category, + that.handleEditLink, + that.options.multiEdit ? mw.msg( 'ajax-confirm-ok' ) : mw.msg( 'ajax-confirm-save' ) + ); - addButton.click( callback ); - promptTextbox.keyup( function( e ) { - if ( e.keyCode == 13 ) addButton.click(); - }); - promptTextbox.suggestions( { - 'fetch': fetchSuggestions, - 'cancel': function() { - var req = _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(); - } - } - } ); + $link.after( $input ).hide(); - promptTextbox.suggestions(); + $input.find( '.mw-addcategory-input' ).focus(); - promptContainer.append( promptTextbox ); - promptContainer.append( addButton ); + $link.data( 'editButton' ).hide(); - return promptContainer; + $link.data( 'deleteButton' ) + .unbind( 'click' ) + .click( function() { + $input.remove(); + $link.show().data( 'editButton' ).show(); + $( this ) + .unbind( 'click' ) + .click( that.handleDeleteLink ) + .attr( 'title', mw.msg( 'ajax-remove-category' ) ); + }) + .attr( 'title', mw.msg( 'ajax-cancel' ) ); }; /** - * Parse the DOM $container and build a list of - * present categories - * - * @return array Array of all categories + * Handle edit category submit. Not to be called directly. + * + * @context Element + * @param e {jQuery Event} */ - this.getCats = function() { - return options.$container.find( options.categoryLinkSelector ).map( function() { return $.trim( $( this ).text() ); } ); - }; + this.handleEditLink = function( e ) { + var categoryNew, + $el = $( this ), + $link = $el.parent().parent().find( 'a:not(.icon)' ), + sortkey = ''; - /** - * Check whether a passed category is present in the DOM - * - * @return boolean True for exists - */ - this.containsCat = function( cat ) { - return that.getCats().filter( function() { return $.ucFirst( this ) == $.ucFirst( cat ); } ).length !== 0; - }; + // Grab category text + categoryNew = $el.parent().find( '.mw-addcategory-input' ).val(); + categoryNew = $.ucFirst( categoryNew.replace( /_/g, ' ' ) ); - /** - * This gets called by all action buttons - * Displays a dialog to confirm the action - * Afterwards do the actual edit - * - * @param function fn text-modifying function - * @param string actionSummary Changes done - * @param string shortSummary Changes, short version - * @param function fn doneFn callback after everything is done - * @return boolean True for exists - */ - this._confirmEdit = function( fn, actionSummary, shortSummary, doneFn, $link, action ) { - // Check whether to use multiEdit mode - if ( options.multiEdit && action != 'all' ) { - // Stash away - $link.data( 'stashIndex', _stash.fns.length ); - $link.data( 'summary', actionSummary ); - _stash.summaries.push( actionSummary ); - _stash.shortSum.push( shortSummary ); - _stash.fns.push( fn ); + // Strip sortkey + var arr = categoryNew.split( '|' ); + if ( arr.length > 1 ) { + categoryNew = arr.shift(); + sortkey = '|' + arr.join( '|' ); + } - _saveAllButton.show(); - _cancelAllButton.show(); + // Grab text + var added = $link.hasClass( 'mw-added-category' ); + that.resetCatLink( $link ); + var category = $link.text(); - // This only does visual changes - doneFn( true ); + // Check for dupes ( exluding itself ) + if ( category !== categoryNew && that.containsCat( categoryNew ) ) { + $link.data( 'deleteButton' ).click(); return; } - // Produce a confirmation dialog - var dialog = $( '
    ' ); - - dialog.addClass( 'mw-ajax-confirm-dialog' ); - dialog.attr( 'title', mw.msg( 'ajax-confirm-title' ) ); - - // Summary of the action to be taken - var summaryHolder = $( '

    ' ); - summaryHolder.html( '' + mw.msg( 'ajax-category-question' ) + '
    ' + actionSummary ); - dialog.append( summaryHolder ); - - // Reason textbox. - var reasonBox = $( '' ); - reasonBox.addClass( 'mw-ajax-confirm-reason' ); - dialog.append( reasonBox ); - - // Submit button - var submitButton = $( '' ); - submitButton.val( mw.msg( 'ajax-confirm-save' ) ); - - var submitFunction = function() { - that._addProgressIndicator( dialog ); - that._doEdit( - mw.config.get( 'wgPageName' ), - fn, - shortSummary + ': ' + reasonBox.val(), - function() { - doneFn(); - dialog.dialog( 'close' ); - that._removeProgressIndicator( dialog ); - } - ); - }; - var buttons = {}; - buttons[mw.msg( 'ajax-confirm-save' )] = submitFunction; - var dialogOptions = { - 'AutoOpen' : true, - 'buttons' : buttons, - 'width' : 450 - }; + // Resolve redirects + that.resolveRedirects( categoryNew, function( resolvedCat, exists ) { + that.handleCategoryEdit( $link, category, resolvedCat, sortkey, exists, added ); + }); + }; - $( '#catlinks' ).prepend( dialog ); - dialog.dialog( dialogOptions ); + /** + * 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(); - // Close on enter - dialog.keyup( function( e ) { - if ( e.keyCode == 13 ) submitFunction(); - }); + if ( $link.is( '.mw-added-category, .mw-changed-category' ) ) { + // We're just cancelling the addition or edit + that.resetCatLink( $link, $link.hasClass( 'mw-added-category' ) ); + return; + } else if ( $link.is( '.mw-removed-category' ) ) { + // It's already removed... + return; + } + that.handleCategoryDelete( $link, category ); }; /** * When multiEdit mode is enabled, * this is called when the user clicks "save all" - * Combines the summaries and edit functions + * Combines the summaries and edit functions. + * + * @context Element + * @return ? */ - this._handleStashedCategories = function() { - var summary = '', fns = _stash.fns; + this.handleStashedCategories = function() { // Remove "holes" in array - summary = $.grep( _stash.summaries, function( n, i ) { - return ( n ); - }); + var summary = $.grep( that.stash.summaries, function( n, i ) { + return n; + } ); + if ( summary.length < 1 ) { // Nothing to do here. - _saveAllButton.hide(); - _cancelAllButton.hide(); + that.saveAllButton.hide(); + that.cancelAllButton.hide(); return; } else { - summary = summary.join( '
    ' ); + summary = summary.join( '
    ' ); } + // Remove "holes" in array - summaryShort = $.grep( _stash.shortSum, function( n,i ) { - return ( n ); - }); + var summaryShort = $.grep( that.stash.shortSum, function( n,i ) { + return n; + } ); summaryShort = summaryShort.join( ', ' ); - var combinedFn = function( oldtext ) { - // Run the text through all action functions - newtext = oldtext; - for ( var i = 0; i < fns.length; i++ ) { - if ( $.isFunction( fns[i] ) ) { - newtext = fns[i]( newtext ); - if ( newtext === false ) { - return false; + var fns = that.stash.fns; + + that.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; - }; - var doneFn = function() { that.resetAll( true ); }; - - that._confirmEdit( combinedFn, summary, summaryShort, doneFn, '', 'all' ); + return newtext; + }, + actionSummary: summary, + shortSummary: summaryShort, + doneFn: function() { + that.resetAll( true ); + }, + $link: null, + action: 'all' + } ); }; +}; + +/* Public methods */ +mw.ajaxCategories.prototype = { /** - * Do the actual edit. - * Gets token & text from api, runs it through fn - * and saves it with summary. - * @param str page Pagename - * @param function fn edit function - * @param str summary - * @param str doneFn Callback after all is done + * Create the UI */ - this._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' - }; + 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, + that = 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(); + }); - $.post( mw.util.wikiScript( 'api' ), getTokenVars, - function( reply ) { - var infos = reply.query.pages; - $.each( - infos, - function( pageid, data ) { - var token = data.edittoken; - var timestamp = data.revisions[0].timestamp; - var oldText = data.revisions[0]['*']; - - // Replace all nowiki and comments with unique keys - var key = mw.user.generateId(); - var nowiki = []; - oldText = replaceNowikis( oldText, key, nowiki ); - - // Then do the changes - var newText = fn( oldText ); - if ( newText === false ) return; - - // And restore them back - newText = restoreNowikis( newText, key, nowiki ); - - 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 ) { - _showError( mw.msg( 'ajax-api-error', text, error ) ); - }); - } - ); - } - , 'json' ).error( function( xhr, text, error ) { - _showError( mw.msg( 'ajax-api-error', text, error ) ); + // 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 li a' ).each( function() { + that.createCatButtons( $( this ) ); }); - }; - /** - * Append spinner wheel to element - * @param DOMObject element. - */ - this._addProgressIndicator = function( elem ) { - elem.append( $( '

    ' ).addClass( 'mw-ajax-loader' ) ); - }; + + 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() { + that.resetAll( false ); + } ).hide(); + options.$containerNormal.append( this.saveAllButton ).append( this.cancelAllButton ); + options.$container.append( this.addContainer ); + }, /** - * Find and remove spinner wheel from inside element - * @param DOMObject parent element. - */ - this._removeProgressIndicator = function( elem ) { - elem.find( '.mw-ajax-loader' ).remove(); - }; - - /** - * Checks the API whether the category in question is a redirect. - * Also returns existance info ( to color link red/blue ) - * @param string category. - * @param function callback + * Insert a newly added category into the DOM. + * + * @param cat {String} Category name. + * @return {jQuery} */ - this._resolveRedirects = function( category, callback ) { - if ( !options.resolveRedirects ) { - callback( category ); + createCatLink: function( cat ) { + // User can implicitely state a sort key. + // Remove before display. + // strip out bad characters + cat = clean( cat.replace( /\|.*/, '' ) ); + + if ( $.isEmpty( cat ) || this.containsCat( cat ) ) { return; } - var queryVars = { - 'action':'query', - 'titles': new mw.Title( category, catNsId ).toString(), - 'redirects':'', - 'format' : 'json' - }; - $.get( mw.util.wikiScript( 'api' ), queryVars, - function( reply ) { - var redirect = reply.query.redirects; - if ( redirect ) { - category = new mw.Title( redirect[0].to )._name; - } - callback( category, !reply.query.pages[-1] ); - } - , 'json' ); - }; - + var $catLinkWrapper = $( this.options.catLinkWrapper ), + $anchor = $( '' ) + .append( cat ) + .attr( { + target: '_blank', + href: catUrl( cat ) + } ); + + $catLinkWrapper.append( $anchor ); + + this.createCatButtons( $anchor ); + + return $anchor; + }, + /** - * Handle add category submit. Not to be called directly + * 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 */ - this._handleAddLink = function( e ) { - var $this = $( this ), $link = $(); + makeSuggestionBox: function( prefill, callback, buttonVal ) { + // Create add category prompt + var $promptContainer = $( '
    ' ), + $promptTextbox = $( '' ), + $addButton = $( '' ), + that = this; - // Grab category text - var category = $this.parent().find( '.mw-addcategory-input' ).val(); - category = $.ucFirst( category ); + 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; + }, - // Resolve redirects - that._resolveRedirects( category, function( resolvedCat, exists ) { - that.handleCategoryAdd( $link, resolvedCat, false, exists ); - } ); - }; /** - * Execute or queue an category add + * Execute or queue an category add. + * @param $link {jQuery} + * @param category + * @param noAppend + * @param exists + * @return {mw.ajaxCategories} */ - this.handleCategoryAdd = function( $link, category, noAppend, exists ) { - if ( !$link.length ) { - $link = that.createCatLink( category ); - } - // Mark red if missing - $link.toggleClass( 'new', exists === false ); - + handleCategoryAdd: function( $link, category, noAppend, exists ) { // Handle sortkey - var arr = category.split( '|' ), sortkey = ''; - + var arr = category.split( '|' ), + sortkey = '', + that = this; + if ( arr.length > 1 ) { category = arr.shift(); sortkey = '|' + arr.join( '|' ); - if ( sortkey == '|' ) sortkey = ''; - } - - //Replace underscores - category = category.replace(/_/g, ' ' ); - - if ( that.containsCat( category ) ) { - _showError( mw.msg( 'ajax-category-already-present', category ) ); - return; + if ( sortkey === '|' ) { + sortkey = ''; + } } - var catFull = new mw.Title( category, catNsId ).toString().replace(/_/g, ' ' ); - var appendText = "\n[[" + catFull + sortkey + "]]\n"; - var summary = mw.msg( 'ajax-add-category-summary', category ); - var shortSummary = '+[[' + catFull + ']]'; - that._confirmEdit( - function( oldText ) { - newText = _runHooks ( oldText, 'beforeAdd', category ); - newText = newText + appendText; - return _runHooks ( newText, 'afterAdd', category ); - }, - summary, - shortSummary, - function( unsaved ) { - if ( !noAppend ) { - options.$container.find( '#mw-normal-catlinks>.mw-addcategory-prompt' ).children( 'input' ).hide(); - 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( category ).attr( 'href', catUrl( category ) ); - } - if ( unsaved ) { - $link.addClass( 'mw-added-category' ); - } - $( '.mw-ajax-addcategory' ).click(); - }, - $link, - 'add' - ); - }; - this._createEditInterface = function( e ) { - var $this = $( this ), - $link = $this.data( 'link' ), - category = $link.text(); - var $input = that._makeSuggestionBox( category, - that._handleEditLink, - options.multiEdit ? mw.msg( 'ajax-confirm-ok' ) : mw.msg( 'ajax-confirm-save' ) - ); - $link.after( $input ).hide(); - $input.find( '.mw-addcategory-input' ).focus(); - $link.data( 'editButton' ).hide(); - $link.data( 'deleteButton' ).unbind( 'click' ).click( function() { - $input.remove(); - $link.show(); - $link.data( 'editButton' ).show(); - $( this ).unbind( 'click' ).click( that._handleDeleteLink ) - .attr( 'title', mw.msg( 'ajax-remove-category' )); - }).attr( 'title', mw.msg( 'ajax-cancel' )); - }; - - /** - * Handle edit category submit. Not to be called directly - */ - this._handleEditLink = function( e ) { - var $this = $( this ), - $link = $this.parent().parent().find( 'a:not(.icon)' ), - categoryNew, sortkey = ''; - - // Grab category text - categoryNew = $this.parent().find( '.mw-addcategory-input' ).val(); - categoryNew = $.ucFirst( categoryNew.replace(/_/g, ' ' ) ); - - // Strip sortkey - var arr = categoryNew.split( '|' ); - if ( arr.length > 1 ) { - categoryNew = arr.shift(); - sortkey = '|' + arr.join( '|' ); + if ( !$link.length ) { + $link = this.createCatLink( category ); } - // Grab text - var added = $link.hasClass( 'mw-added-category' ); - that.resetCatLink ( $link ); - var category = $link.text(); + if ( this.containsCat( category ) ) { + this.showError( mw.msg( 'ajax-category-already-present', category ) ); + return this; + } - // Check for dupes ( exluding itself ) - if ( category != categoryNew && that.containsCat( categoryNew ) ) { - $link.data( 'deleteButton' ).click(); - return; + // Sometimes createCatLink returns undefined/null, previously caused an exception + // in the following lines, catching now.. @todo + if ( !$link ) { + this.showError( 'Unexpected error occurred. $link undefined.' ); + return this; } - // Resolve redirects - that._resolveRedirects( categoryNew, function( resolvedCat, exists ) { - that.handleCategoryEdit( $link, category, resolvedCat, sortkey, exists, added ); - }); - }; + // Mark red if missing + $link.toggleClass( 'new', exists !== true ); + + // Replace underscores + category = category.replace( /_/g, ' ' ); + var catFull = new mw.Title( category, catNsId ).toString().replace( /_/g, ' ' ); + + this.doConfirmEdit( { + modFn: function( oldText ) { + var newText = that.runHooks( oldText, 'beforeAdd', category ); + newText = newText + "\n[[" + catFull + sortkey + "]]\n"; + return that.runHooks( newText, 'afterAdd', category ); + }, + actionSummary: mw.msg( 'ajax-add-category-summary', category ), + shortSummary: '+[[' + catFull + ']]', + doneFn: function( unsaved ) { + if ( !noAppend ) { + that.options.$container + .find( '#mw-normal-catlinks > .mw-addcategory-prompt' ).children( 'input' ).hide(); + that.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( category ).attr( 'href', catUrl( category ) ); + } + if ( unsaved ) { + $link.addClass( 'mw-added-category' ); + } + $( '.mw-ajax-addcategory' ).click(); + }, + $link: $link, + action: 'add' + } ); + return this; + }, + /** - * Execute or queue an category edit + * Execute or queue an category edit. + * @param $link {jQuery} + * @param category + * @param categoryNew + * @param sortkeyNew + * @param exists {Boolean} + * @param added {Boolean} */ - this.handleCategoryEdit = function( $link, category, categoryNew, sortkeyNew, exists, added ) { + handleCategoryEdit: function( $link, category, categoryNew, sortkeyNew, exists, added ) { + var that = this; + // Category add needs to be handled differently if ( added ) { // Pass sortkey back that.handleCategoryAdd( $link, categoryNew + sortkeyNew, true ); return; } + // User didn't change anything. - if ( category == categoryNew + sortkeyNew ) { + if ( category === categoryNew + sortkeyNew ) { $link.data( 'deleteButton' ).click(); return; } + // Mark red if missing - $link.toggleClass( 'new', exists === false ); - - categoryRegex = buildRegex( category ); - - var summary = mw.msg( 'ajax-edit-category-summary', category, categoryNew ); - var shortSummary = '[[' + new mw.Title( category, catNsId ) + ']] -> [[' + new mw.Title( categoryNew, catNsId ) + ']]'; - that._confirmEdit( - function( oldText ) { - newText = _runHooks ( oldText, 'beforeChange', category, categoryNew ); - - var matches = newText.match( categoryRegex ); + $link[(exists === false ? 'addClass' : 'removeClass')]( 'new' ); + + var categoryRegex = buildRegex( category ), + shortSummary = '[[' + new mw.Title( category, catNsId ) + ']] -> [[' + new mw.Title( categoryNew, catNsId ) + ']]'; + that.doConfirmEdit({ + modFn: function( oldText ) { + var sortkey, newCategoryString, + newText = that.runHooks( oldText, 'beforeChange', category, categoryNew ), + matches = newText.match( categoryRegex ); //Old cat wasn't found, likely to be transcluded if ( !$.isArray( matches ) ) { - _showError( mw.msg( 'ajax-edit-category-error' ) ); + that.showError( mw.msg( 'ajax-edit-category-error' ) ); return false; } - var sortkey = sortkeyNew || matches[0].replace( categoryRegex, '$2' ); - var newCategoryString = "[[" + new mw.Title( categoryNew, catNsId ) + sortkey + ']]'; + sortkey = sortkeyNew || matches[0].replace( categoryRegex, '$2' ); + newCategoryString = '[[' + new mw.Title( categoryNew, catNsId ) + sortkey + ']]'; 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], '' ); + oldText = oldText.replace( matches[i], '' ); } } - var newText = oldText.replace( categoryRegex, newCategoryString ); + newText = oldText.replace( categoryRegex, newCategoryString ); - return _runHooks ( newText, 'afterChange', category, categoryNew ); + return that.runHooks( newText, 'afterChange', category, categoryNew ); }, - summary, - shortSummary, - function( unsaved ) { + actionSummary: mw.msg( 'ajax-edit-category-summary', category, categoryNew ), + shortSummary: shortSummary, + doneFn: function( unsaved ) { // Remove input box & button $link.data( 'deleteButton' ).click(); @@ -710,246 +660,397 @@ mw.ajaxCategories = function( options ) { $link.data( 'origCat', category ).addClass( 'mw-changed-category' ); } }, - $link, - 'edit' - ); - }; - + $link: $link, + action: 'edit' + }); + }, + /** - * Handle delete category submit. Not to be called directly + * Checks the API whether the category in question is a redirect. + * Also returns existance info (to color link red/blue) + * @param string category. + * @param function callback */ - this._handleDeleteLink = function() { - var $this = $( this ), - $link = $this.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 - that.resetCatLink ( $link, $link.hasClass( 'mw-added-category' ) ); - return; - } else if ( $link.is( '.mw-removed-category' ) ) { - // It's already removed... + resolveRedirects: function( category, callback ) { + if ( !this.options.resolveRedirects ) { + callback( category, true ); return; } - that.handleCategoryDelete( $link, category ); - }; - + var queryVars = { + action:'query', + titles: new mw.Title( category, catNsId ).toString(), + redirects: '', + format: 'json' + }; + + $.get( mw.util.wikiScript( 'api' ), queryVars, + function( reply ) { + var redirect = reply.query.redirects; + if ( redirect ) { + category = new mw.Title( redirect[0].to )._name; + } + callback( category, !reply.query.pages[-1] ); + }, 'json' + ); + }, + + /** + * 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(), + that = this; + + deleteButton.click( this.handleDeleteLink ); + editButton.click( that.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; + }, + /** - * Execute or queue an category delete + * 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. + * + * @return {Boolean} */ - this.handleCategoryDelete = function( $link, category ) { - var categoryRegex = buildRegex( category, true ); + containsCat: function( cat ) { + cat = $.ucFirst( cat ); + return this.getCats().filter( function() { + return $.ucFirst( this ) === cat; + } ).length !== 0; + }, - var summary = mw.msg( 'ajax-remove-category-summary', category ); - var shortSummary = '-[[' + new mw.Title( category, catNsId ) + ']]'; + /** + * Execute or queue an category delete. + * + * @param $link {jQuery} + * @param category + * @return ? + */ + handleCategoryDelete: function( $link, category ) { + var categoryRegex = buildRegex( category, true ), + that = this; - that._confirmEdit( - function( oldText ) { - newText = _runHooks ( oldText, 'beforeDelete', category ); - var newText = newText.replace( categoryRegex, '' ); + that.doConfirmEdit({ + modFn: function( oldText ) { + var newText = that.runHooks( oldText, 'beforeDelete', category ); + newText = newText.replace( categoryRegex, '' ); - if ( newText == oldText ) { - _showError( mw.msg( 'ajax-remove-category-error' ) ); + if ( newText === oldText ) { + that.showError( mw.msg( 'ajax-remove-category-error' ) ); return false; } - return _runHooks ( newText, 'afterDelete', category ); + return that.runHooks( newText, 'afterDelete', category ); }, - summary, - shortSummary, - function( unsaved ) { + actionSummary: mw.msg( 'ajax-remove-category-summary', category ), + shortSummary: '-[[' + new mw.Title( category, catNsId ) + ']]', + doneFn: function( unsaved ) { if ( unsaved ) { $link.addClass( 'mw-removed-category' ); } else { $link.parent().remove(); } }, - $link, - 'delete' - ); - }; - + $link: $link, + action: 'delete' + }); + }, /** - * Open a dismissable error dialog + * Takes a category link element + * and strips all data from it. * - * @param string str The error description + * @param $link {jQuery} + * @param del {Boolean} + * @param dontRestoreText {Boolean} + * @return ? */ - _showError = function( str ) { - var oldDialog = $( '.mw-ajax-confirm-dialog' ); - that._removeProgressIndicator( oldDialog ); - oldDialog.dialog( 'close' ); - - var dialog = $( '
    ' ); - dialog.text( str ); - - mw.util.$content.append( dialog ); + resetCatLink: function( $link, del, dontRestoreText ) { + $link.removeClass( 'mw-removed-category mw-added-category mw-changed-category' ); + var data = $link.data(); - var buttons = { }; - buttons[mw.msg( 'ajax-confirm-ok' )] = function( e ) { - dialog.dialog( 'close' ); - }; - var dialogOptions = { - 'buttons' : buttons, - 'AutoOpen' : true, - 'title' : mw.msg( 'ajax-error-title' ) - }; + if ( typeof data.stashIndex === 'number' ) { + this.removeStashItem( data.stashIndex ); + } + if ( del ) { + $link.parent.remove(); + return; + } + if ( data.origCat && !dontRestoreText ) { + $link.text( data.origCat ); + $link.attr( 'href', catUrl( data.origCat ) ); + } - dialog.dialog( dialogOptions ); + $link.removeData(); - // Close on enter - dialog.keyup( function( e ) { - if ( e.keyCode == 13 ) dialog.dialog( 'close' ); - }); - }; + // Readd static. + $link.data( { + saveButton: data.saveButton, + deleteButton: data.deleteButton, + editButton: data.editButton + } ); + }, /** - * Manufacture iconed button, with or without text - * - * @param string icon The icon class. - * @param string title Title attribute. - * @param string className (optional) Additional classes to be added to the button. - * @param string text (optional) Text of button. - * - * @return jQueryObject The button + * 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 */ - _createButton = function( 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( '​' ); + 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' + }, that = this; + + $.post( + mw.util.wikiScript( 'api' ), + getTokenVars, + function( reply ) { + var infos = reply.query.pages; + $.each( infos, function( pageid, data ) { + var token = data.edittoken; + var timestamp = data.revisions[0].timestamp; + var oldText = data.revisions[0]['*']; + + // Replace all nowiki and comments with unique keys + var key = mw.user.generateId(); + var nowiki = []; + oldText = replaceNowikis( oldText, key, nowiki ); + + // Then do the changes + var newText = fn( oldText ); + if ( newText === false ) { + return; + } - if ( text ) { - var $icon = $( '' ).addClass( 'icon ' + icon ).html( '​' ); - $button.addClass( 'icon-parent' ).append( $icon ).append( text ); - } else { - $button.addClass( 'icon ' + icon ); - } - return $button; - }; + // And restore them back + newText = restoreNowikis( newText, key, nowiki ); + + 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 ) { + that.showError( mw.msg( 'ajax-api-error', text, error ) ); + }); + } ); + }, + 'json' + ).error( function( xhr, text, error ) { + that.showError( mw.msg( 'ajax-api-error', text, error ) ); + } ); + }, /** - * Append edit and remove buttons to a given category link + * This gets called by all action buttons + * Displays a dialog to confirm the action + * Afterwards do the actual edit. * - * @param DOMElement element Anchor element, to which the buttons should be appended. + * @param props {Object}: + * - modFn {Function} text-modifying function + * - actionSummary {String} Changes done + * - shortSummary {String} Changes, short version + * - doneFn {Function} callback after everything is done + * - $link {jQuery} + * - action + * @return {mw.ajaxCategories} */ - _createCatButtons = function( $element ) { - // Create remove & edit buttons - var deleteButton = _createButton( 'icon-close', mw.msg( 'ajax-remove-category' ) ); - var editButton = _createButton( 'icon-edit', mw.msg( 'ajax-edit-category' ) ); + doConfirmEdit: function( props ) { + var summaryHolder, reasonBox, dialog, submitFunction, + buttons = {}, + dialogOptions = { + AutoOpen: true, + buttons: buttons, + width: 450 + }, + that = this; - //Not yet used - var saveButton = _createButton( 'icon-tick', mw.msg( 'ajax-confirm-save' ) ).hide(); + // Check whether to use multiEdit mode: + if ( this.options.multiEdit && props.action !== 'all' ) { - deleteButton.click( that._handleDeleteLink ); - editButton.click( that._createEditInterface ); + // Stash away + props.$link + .data( 'stashIndex', this.stash.fns.length ) + .data( 'summary', props.actionSummary ); - $element.after( deleteButton ).after( editButton ); + this.stash.summaries.push( props.actionSummary ); + this.stash.shortSum.push( props.shortSummary ); + this.stash.fns.push( props.modFn ); - //Save references to all links and buttons - $element.data({ - saveButton : saveButton, - deleteButton: deleteButton, - editButton : editButton - }); - editButton.data({ - link : $element - }); - }; + this.saveAllButton.show(); + this.cancelAllButton.show(); - /** - * Create the UI - */ - this.setup = function() { - // Could be set by gadgets like HotCat etc. - if ( mw.config.get( 'disableAJAXCategories' ) ) { - return false; + // Clear input field after action + that.addContainer.find( '.mw-addcategory-input' ).val( '' ); + + // This only does visual changes, fire done and return. + props.doneFn( true ); + return this; } - // Only do it for articles. - if ( !mw.config.get( 'wgIsArticle' ) ) return; - - // Create [Add Category] link - var addLink = _createButton( 'icon-add', - mw.msg( 'ajax-add-category' ), - 'mw-ajax-addcategory', - mw.msg( 'ajax-add-category' ) - ); - addLink.click( function() { - $( this ).nextAll().toggle().filter( '.mw-addcategory-input' ).focus(); - }); - - // Create add category prompt - _addContainer = that._makeSuggestionBox( '', that._handleAddLink, mw.msg( 'ajax-add-category-submit' ) ); - _addContainer.children().hide(); + // Summary of the action to be taken + summaryHolder = $( '

    ' ) + .html( '' + mw.msg( 'ajax-category-question' ) + '
    ' + props.actionSummary ); - _addContainer.prepend( addLink ); + // Reason textbox. + reasonBox = $( '' ) + .addClass( 'mw-ajax-confirm-reason' ); - // Create edit & delete link for each category. - $( '#catlinks li a' ).each( function() { - _createCatButtons( $( this ) ); - }); + // Produce a confirmation dialog + dialog = $( '

    ' ) + .addClass( 'mw-ajax-confirm-dialog' ) + .attr( 'title', mw.msg( 'ajax-confirm-title' ) ) + .append( summaryHolder ) + .append( reasonBox ); - options.$containerNormal.append( _addContainer ); - - //TODO Make more clickable - _saveAllButton = _createButton( 'icon-tick', - mw.msg( 'ajax-confirm-save-all' ), - '', - mw.msg( 'ajax-confirm-save-all' ) - ); - _cancelAllButton = _createButton( 'icon-close', - mw.msg( 'ajax-cancel-all' ), - '', - mw.msg( 'ajax-cancel-all' ) - ); - _saveAllButton.click( that._handleStashedCategories ).hide(); - _cancelAllButton.click( function() { that.resetAll( false ); } ).hide(); - options.$containerNormal.append( _saveAllButton ).append( _cancelAllButton ); - options.$container.append( _addContainer ); - }; + // Submit button + submitFunction = function() { + that.addProgressIndicator( dialog ); + that.doEdit( + mw.config.get( 'wgPageName' ), + props.modFn, + props.shortSummary + ': ' + reasonBox.val(), + function() { + props.doneFn(); - _stash = { - summaries : [], - shortSum : [], - fns : [] - }; - _removeStashItem = function( i ) { - if ( typeof i != "number" ) { + // Clear input field after successful edit + that.addContainer.find( '.mw-addcategory-input' ).val( '' ); + + dialog.dialog( 'close' ); + that.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' ); } - delete _stash.fns[i]; - delete _stash.summaries[i]; - if ( $.isEmpty( _stash.fns ) ) { - _stash.fns = []; - _stash.summaries = []; - _stash.shortSum = []; - _saveAllButton.hide(); - _cancelAllButton.hide(); + + try { + delete this.stash.fns[i]; + delete this.stash.summaries[i]; + } catch(e) {} + + if ( $.isEmpty( this.stash.fns ) ) { + this.stash.fns = []; + this.stash.summaries = []; + this.stash.shortSum = []; + this.saveAllButton.hide(); + this.cancelAllButton.hide(); } - }; - _hooks = { - beforeAdd : [], - beforeChange : [], - beforeDelete : [], - afterAdd : [], - afterChange : [], - afterDelete : [] - }; - _runHooks = function( oldtext, type, category, categoryNew ) { - // No hooks registered - if ( !_hooks[type] ) { - return oldtext; - } else { - for ( var i = 0; i < _hooks[type].length; i++ ) { - oldtext = _hooks[type][i]( oldtext, category, categoryNew ); - if ( oldtext === false ) { - _showError( mw.msg( 'ajax-category-hook-error', category ) ); - return; - } - } - return oldtext; + 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 = $([]), + that = this; + + if ( del ) { + $del = $links.filter( '.mw-removed-category' ).parent(); } - }; + + $links.each( function() { + that.resetCatLink( $( this ), false, del ); + } ); + + $del.remove(); + + this.options.$container.find( '#mw-hidden-catlinks' ).remove(); + + return this; + }, + /** * Add hooks * Currently available: beforeAdd, beforeChange, beforeDelete, @@ -958,18 +1059,74 @@ mw.ajaxCategories = function( options ) { * * @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 + * 1. oldtext: The wikitext before the hook + * 2. category: The deleted, added, or changed category + * 3. (only for beforeChange/afterChange): newcategory */ - this.addHook = function( type, fn ) { - if ( !_hooks[type] || !$.isFunction( fn ) ) { + addHook: function( type, fn ) { + if ( !this.hooks[type] || !$.isFunction( fn ) ) { return; } else { - hooks[type].push( fn ); + 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 ); \ No newline at end of file +} )( jQuery ); diff --git a/resources/mediawiki.page/mediawiki.page.startup.js b/resources/mediawiki.page/mediawiki.page.startup.js index 0a8cabf47e..163dbf3f53 100644 --- a/resources/mediawiki.page/mediawiki.page.startup.js +++ b/resources/mediawiki.page/mediawiki.page.startup.js @@ -1,5 +1,7 @@ ( function( $ ) { + mw.page = {}; + /* Client profile classes for */ var prof = $.client.profile(); -- 2.20.1