Index: trunk/phase3/skins/common/images/remove.png |
Cannot display: file marked as a binary type. |
svn:mime-type = application/octet-stream |
Property changes on: trunk/phase3/skins/common/images/remove.png |
___________________________________________________________________ |
Added: svn:executable |
1 | 1 | + * |
Added: svn:mime-type |
2 | 2 | + application/octet-stream |
Index: trunk/phase3/skins/common/images/add.png |
Cannot display: file marked as a binary type. |
svn:mime-type = application/octet-stream |
Property changes on: trunk/phase3/skins/common/images/add.png |
___________________________________________________________________ |
Added: svn:executable |
3 | 3 | + * |
Added: svn:mime-type |
4 | 4 | + application/octet-stream |
Index: trunk/phase3/skins/common/shared.css |
— | — | @@ -772,6 +772,30 @@ |
773 | 773 | font-family:monospace |
774 | 774 | } |
775 | 775 | |
| 776 | +#mw-addcategory-prompt { |
| 777 | + display: inline; |
| 778 | + margin-left: 1em; |
| 779 | +} |
| 780 | + |
| 781 | +#mw-addcategory-prompt input { |
| 782 | + margin-left: 0.5em; |
| 783 | + margin-right: 0.5em; |
| 784 | +} |
| 785 | + |
| 786 | +.mw-remove-category { |
| 787 | + padding: 8px; |
| 788 | + background-image: url(images/remove.png); |
| 789 | + background-position: center center; |
| 790 | + background-repeat: no-repeat; |
| 791 | +} |
| 792 | + |
| 793 | +.mw-ajax-addcategory { |
| 794 | + padding-left: 20px; |
| 795 | + background-image: url(images/add.png); |
| 796 | + background-position: left center; |
| 797 | + background-repeat: no-repeat; |
| 798 | +} |
| 799 | + |
776 | 800 | .mw-ajax-loader { |
777 | 801 | background-image: url(images/ajax-loader.gif); |
778 | 802 | background-position: center center; |
Index: trunk/phase3/includes/Xml.php |
— | — | @@ -567,7 +567,9 @@ |
568 | 568 | $s = 'null'; |
569 | 569 | } elseif ( is_int( $value ) ) { |
570 | 570 | $s = $value; |
571 | | - } elseif ( is_array( $value ) ) { |
| 571 | + } elseif ( is_array( $value ) && // Make sure it's not associative. |
| 572 | + array_keys($value) === range(0,count($value)-1) |
| 573 | + ) { |
572 | 574 | $s = '['; |
573 | 575 | foreach ( $value as $elt ) { |
574 | 576 | if ( $s != '[' ) { |
— | — | @@ -576,7 +578,8 @@ |
577 | 579 | $s .= self::encodeJsVar( $elt ); |
578 | 580 | } |
579 | 581 | $s .= ']'; |
580 | | - } elseif ( is_object( $value ) ) { |
| 582 | + } elseif ( is_object( $value ) || is_array( $value ) ) { |
| 583 | + // Objects and associative arrays |
581 | 584 | $s = '{'; |
582 | 585 | foreach ( (array)$value as $name => $elt ) { |
583 | 586 | if ( $s != '{' ) { |
Index: trunk/phase3/includes/OutputPage.php |
— | — | @@ -1115,6 +1115,11 @@ |
1116 | 1116 | if( $wgUser->getBoolOption( 'editsectiononrightclick' ) ) { |
1117 | 1117 | $this->addScriptFile( 'rightclickedit.js' ); |
1118 | 1118 | } |
| 1119 | + |
| 1120 | + global $wgUseAJAXCategories; |
| 1121 | + if ($wgUseAJAXCategories) { |
| 1122 | + $this->addScriptClass( 'ajaxCategories' ); |
| 1123 | + } |
1119 | 1124 | |
1120 | 1125 | if( $wgUniversalEditButton ) { |
1121 | 1126 | if( isset( $wgArticle ) && $this->getTitle() && $this->getTitle()->quickUserCan( 'edit' ) |
Index: trunk/phase3/includes/AutoLoader.php |
— | — | @@ -619,6 +619,7 @@ |
620 | 620 | // phase 2 javascript: |
621 | 621 | 'uploadPage' => 'js2/uploadPage.js', |
622 | 622 | 'editPage' => 'js2/editPage.js', |
| 623 | + 'ajaxCategories' => 'js2/ajaxcategories.js', |
623 | 624 | ); |
624 | 625 | |
625 | 626 | //Include the js2 autoLoadClasses |
Index: trunk/phase3/includes/DefaultSettings.php |
— | — | @@ -4195,3 +4195,8 @@ |
4196 | 4196 | * The minimum amount of memory that MediaWiki "needs"; MediaWiki will try to raise PHP's memory limit if it's below this amount. |
4197 | 4197 | */ |
4198 | 4198 | $wgMemoryLimit = "50M"; |
| 4199 | + |
| 4200 | +/** |
| 4201 | + * Whether or not to use the AJAX categories system. |
| 4202 | + */ |
| 4203 | +$wgUseAJAXCategories = false; |
Index: trunk/phase3/includes/Skin.php |
— | — | @@ -409,6 +409,8 @@ |
410 | 410 | 'wgSeparatorTransformTable' => $compactSeparatorTransTable, |
411 | 411 | 'wgDigitTransformTable' => $compactDigitTransTable, |
412 | 412 | 'wgMainPageTitle' => $mainPage ? $mainPage->getPrefixedText() : null, |
| 413 | + 'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(), |
| 414 | + 'wgNamespaceIds' => $wgContLang->getNamespaceIds(), |
413 | 415 | ); |
414 | 416 | if ( $wgContLang->hasVariants() ) { |
415 | 417 | $vars['wgUserVariant'] = $wgContLang->getPreferredVariant(); |
Index: trunk/phase3/js2/mwEmbed/jquery/plugins/jquery.suggestions.js |
— | — | @@ -0,0 +1,459 @@ |
| 2 | +/** |
| 3 | + * This plugin provides a generic way to add suggestions to a text box |
| 4 | + * Usage: |
| 5 | + * |
| 6 | + * Set options |
| 7 | + * $('#textbox').suggestions({ option1: value1, option2: value2 }); |
| 8 | + * $('#textbox').suggestions( option, value ); |
| 9 | + * Get option: |
| 10 | + * value = $('#textbox').suggestions( option ); |
| 11 | + * Initialize: |
| 12 | + * $('#textbox').suggestions(); |
| 13 | + * |
| 14 | + * Available options: |
| 15 | + * animationDuration: How long (in ms) the animated growing of the results box |
| 16 | + * should take (default: 200) |
| 17 | + * cancelPending(): Function called when any pending asynchronous suggestions |
| 18 | + * fetches should be canceled (optional). Executed in the context of the |
| 19 | + * textbox |
| 20 | + * delay: Number of ms to wait for the user to stop typing (default: 120) |
| 21 | + * fetch(query): Callback that should fetch suggestions and set the suggestions |
| 22 | + * property (required). Executed in the context of the textbox |
| 23 | + * maxGrowFactor: Maximum width of the suggestions box as a factor of the width |
| 24 | + * of the textbox (default: 2) |
| 25 | + * maxRows: Maximum number of suggestion rows to show |
| 26 | + * submitOnClick: If true, submit the form when a suggestion is clicked |
| 27 | + * (default: false) |
| 28 | + * suggestions: Array of suggestions to display (default: []) |
| 29 | + * |
| 30 | + */ |
| 31 | +(function($) { |
| 32 | +$.fn.suggestions = function( param, param2 ) { |
| 33 | + /** |
| 34 | + * Handle special keypresses (arrow keys and escape) |
| 35 | + * @param key Key code |
| 36 | + */ |
| 37 | + function processKey( key ) { |
| 38 | + switch ( key ) { |
| 39 | + case 40: |
| 40 | + // Arrow down |
| 41 | + if ( conf._data.div.is( ':visible' ) ) { |
| 42 | + highlightResult( 'next', true ); |
| 43 | + } else { |
| 44 | + // Load suggestions right now |
| 45 | + updateSuggestions( false ); |
| 46 | + } |
| 47 | + break; |
| 48 | + case 38: |
| 49 | + // Arrow up |
| 50 | + if ( conf._data.div.is( ':visible' ) ) { |
| 51 | + highlightResult( 'prev', true ); |
| 52 | + } |
| 53 | + break; |
| 54 | + case 27: |
| 55 | + // Escape |
| 56 | + conf._data.div.hide(); |
| 57 | + restoreText(); |
| 58 | + cancelPendingSuggestions(); |
| 59 | + break; |
| 60 | + default: |
| 61 | + updateSuggestions( true ); |
| 62 | + } |
| 63 | + } |
| 64 | + |
| 65 | + /** |
| 66 | + * Restore the text the user originally typed in the textbox, |
| 67 | + * before it was overwritten by highlightResult(). This restores the |
| 68 | + * value the currently displayed suggestions are based on, rather than |
| 69 | + * the value just before highlightResult() overwrote it; the former |
| 70 | + * is arguably slightly more sensible. |
| 71 | + */ |
| 72 | + function restoreText() { |
| 73 | + conf._data.textbox.val( conf._data.prevText ); |
| 74 | + } |
| 75 | + |
| 76 | + /** |
| 77 | + * Ask the user-specified callback for new suggestions. Any previous |
| 78 | + * delayed call to this function still pending will be canceled. |
| 79 | + * If the value in the textbox hasn't changed since the last time |
| 80 | + * suggestions were fetched, this function does nothing. |
| 81 | + * @param delayed If true, delay this by the user-specified delay |
| 82 | + */ |
| 83 | + function updateSuggestions( delayed ) { |
| 84 | + // Cancel previous call |
| 85 | + if ( conf._data.timerID != null ) |
| 86 | + clearTimeout( conf._data.timerID ); |
| 87 | + if ( delayed ) |
| 88 | + setTimeout( doUpdateSuggestions, conf.delay ); |
| 89 | + else |
| 90 | + doUpdateSuggestions(); |
| 91 | + } |
| 92 | + |
| 93 | + /** |
| 94 | + * Delayed part of updateSuggestions() |
| 95 | + * Don't call this, use updateSuggestions( false ) instead |
| 96 | + */ |
| 97 | + function doUpdateSuggestions() { |
| 98 | + if ( conf._data.textbox.val() == conf._data.prevText ) |
| 99 | + // Value in textbox didn't change |
| 100 | + return; |
| 101 | + |
| 102 | + conf._data.prevText = conf._data.textbox.val(); |
| 103 | + conf.fetch.call ( conf._data.textbox, |
| 104 | + conf._data.textbox.val() ); |
| 105 | + } |
| 106 | + |
| 107 | + /** |
| 108 | + * Called when the user changes the suggestions post-init. |
| 109 | + * Typically happens asynchronously from conf.fetch() |
| 110 | + */ |
| 111 | + function suggestionsChanged() { |
| 112 | + conf._data.div.show(); |
| 113 | + updateSuggestionsTable(); |
| 114 | + fitContainer(); |
| 115 | + trimResultText(); |
| 116 | + } |
| 117 | + |
| 118 | + /** |
| 119 | + * Cancel any delayed updateSuggestions() call and inform the user so |
| 120 | + * they can cancel their result fetching if they use AJAX or something |
| 121 | + */ |
| 122 | + function cancelPendingSuggestions() { |
| 123 | + if ( conf._data.timerID != null ) |
| 124 | + clearTimeout( conf._data.timerID ); |
| 125 | + conf.cancelPending.call( this ); |
| 126 | + } |
| 127 | + |
| 128 | + /** |
| 129 | + * Rebuild the suggestions table |
| 130 | + */ |
| 131 | + function updateSuggestionsTable() { |
| 132 | + // If there are no suggestions, hide the div |
| 133 | + if ( conf.suggestions.length == 0 ) { |
| 134 | + conf._data.div.hide(); |
| 135 | + return; |
| 136 | + } |
| 137 | + |
| 138 | + var table = conf._data.div.children( 'table' ); |
| 139 | + table.empty(); |
| 140 | + for ( var i = 0; i < conf.suggestions.length; i++ ) { |
| 141 | + var td = $( '<td />' ) // FIXME: why use a span? |
| 142 | + .append( $( '<span />' ).text( conf.suggestions[i] ) ); |
| 143 | + //.addClass( 'os-suggest-result' ); //FIXME: use descendant selector |
| 144 | + $( '<tr />' ) |
| 145 | + .addClass( 'os-suggest-result' ) // FIXME: use descendant selector |
| 146 | + .attr( 'rel', i ) |
| 147 | + .data( 'text', conf.suggestions[i] ) |
| 148 | + .append( td ) |
| 149 | + .appendTo( table ); |
| 150 | + } |
| 151 | + } |
| 152 | + |
| 153 | + /** |
| 154 | + * Make the container fit into the screen |
| 155 | + */ |
| 156 | + function fitContainer() { |
| 157 | + if ( conf._data.div.is( ':hidden' ) ) |
| 158 | + return; |
| 159 | + |
| 160 | + // FIXME: Mysterious -20 from mwsuggest.js, |
| 161 | + // presumably to make room for a scrollbar |
| 162 | + var availableHeight = $( 'body' ).height() - ( |
| 163 | + Math.round( conf._data.div.offset().top ) - |
| 164 | + $( document ).scrollTop() ) - 20; |
| 165 | + var rowHeight = conf._data.div.find( 'tr' ).outerHeight(); |
| 166 | + var numRows = Math.floor( availableHeight / rowHeight ); |
| 167 | + |
| 168 | + // Show at least 2 rows if there are multiple results |
| 169 | + if ( numRows < 2 && conf.suggestions.length >= 2 ) |
| 170 | + numRows = 2; |
| 171 | + if ( numRows > conf.maxRows ) |
| 172 | + numRows = conf.maxRows; |
| 173 | + |
| 174 | + var tableHeight = conf._data.div.find( 'table' ).outerHeight(); |
| 175 | + if ( numRows * rowHeight < tableHeight ) { |
| 176 | + // The container is too small |
| 177 | + conf._data.div.height( numRows * rowHeight ); |
| 178 | + conf._data.visibleResults = numRows; |
| 179 | + } else { |
| 180 | + // The container is possibly too large |
| 181 | + conf._data.div.height( tableHeight ); |
| 182 | + conf._data.visibleResults = conf.suggestions.length; |
| 183 | + } |
| 184 | + } |
| 185 | + |
| 186 | + /** |
| 187 | + * If there are results wider than the container, try to grow the |
| 188 | + * container or trim them to end with "..." |
| 189 | + */ |
| 190 | + function trimResultText() { |
| 191 | + if ( conf._data.div.is( ':hidden' ) ) |
| 192 | + return; |
| 193 | + |
| 194 | + // Try to grow the container so all results fit |
| 195 | + // Can't use each() here because the inner function can read |
| 196 | + // but not write maxWidth for some crazy reason |
| 197 | + var maxWidth = 0; |
| 198 | + var spans = conf._data.div.find( 'span' ).get(); |
| 199 | + for ( var i = 0; i < spans.length; i++ ) |
| 200 | + if ( $(spans[i]).outerWidth() > maxWidth ) |
| 201 | + maxWidth = $(spans[i]).outerWidth(); |
| 202 | + |
| 203 | + // FIXME: Some mysterious fixing going on here |
| 204 | + // FIXME: Left out Opera fix for now |
| 205 | + // FIXME: This doesn't check that the container won't run off the screen |
| 206 | + // FIXME: This should try growing to the left instead if no space on the right |
| 207 | + var fix = 0; |
| 208 | + if ( conf._data.visibleResults < conf.suggestions.length ) |
| 209 | + fix = 20; |
| 210 | + //else |
| 211 | + // fix = operaWidthFix(); |
| 212 | + if ( fix < 4 ) |
| 213 | + // FIXME: Make 4px configurable? |
| 214 | + fix = 4; // Always pad at least 4px |
| 215 | + maxWidth += fix; |
| 216 | + |
| 217 | + var textBoxWidth = conf._data.textbox.outerWidth(); |
| 218 | + var factor = maxWidth / textBoxWidth; |
| 219 | + if ( factor > conf.maxGrowFactor ) |
| 220 | + factor = conf.maxGrowFactor; |
| 221 | + if ( factor < 1 ) |
| 222 | + // Don't shrink the container to be smaller |
| 223 | + // than the textbox |
| 224 | + factor = 1; |
| 225 | + var newWidth = Math.round( textBoxWidth * factor ); |
| 226 | + if ( newWidth != conf._data.div.outerWidth() ) |
| 227 | + conf._data.div.animate( { width: newWidth }, |
| 228 | + conf.animationDuration ); |
| 229 | + // FIXME: mwsuggest.js has this inside the if != block |
| 230 | + // but I don't think that's right |
| 231 | + newWidth -= fix; |
| 232 | + |
| 233 | + // If necessary, trim and add ... |
| 234 | + conf._data.div.find( 'tr' ).each( function() { |
| 235 | + var span = $(this).find( 'span' ); |
| 236 | + if ( span.outerWidth() > newWidth ) { |
| 237 | + var span = $(this).find( 'span' ); |
| 238 | + span.text( span.text() + '...' ); |
| 239 | + |
| 240 | + // While it's still too wide and the last |
| 241 | + // iteration shrunk it, remove the character |
| 242 | + // before '...' |
| 243 | + while ( span.outerWidth() > newWidth && span.text().length > 3 ) { |
| 244 | + span.text( span.text().substring( 0, |
| 245 | + span.text().length - 4 ) + '...' ); |
| 246 | + } |
| 247 | + $(this).attr( 'title', $(this).data( 'text' ) ); |
| 248 | + } |
| 249 | + }); |
| 250 | + } |
| 251 | + |
| 252 | + /** |
| 253 | + * Get a jQuery object for the currently highlighted row |
| 254 | + */ |
| 255 | + function getHighlightedRow() { |
| 256 | + return conf._data.div.find( '.os-suggest-result-hl' ); |
| 257 | + } |
| 258 | + |
| 259 | + /** |
| 260 | + * Highlight a result in the results table |
| 261 | + * @param result <tr> to highlight: jQuery object, or 'prev' or 'next' |
| 262 | + * @param updateTextbox If true, put the suggestion in the textbox |
| 263 | + */ |
| 264 | + function highlightResult( result, updateTextbox ) { |
| 265 | + // TODO: Use our own class here |
| 266 | + var selected = getHighlightedRow(); |
| 267 | + if ( !result.get || selected.get( 0 ) != result.get( 0 ) ) { |
| 268 | + if ( result == 'prev' ) { |
| 269 | + result = selected.prev(); |
| 270 | + } else if ( result == 'next' ) { |
| 271 | + if ( selected.size() == 0 ) |
| 272 | + // No item selected, go to the first one |
| 273 | + result = conf._data.div.find( 'tr:first' ); |
| 274 | + else { |
| 275 | + result = selected.next(); |
| 276 | + if ( result.size() == 0 ) |
| 277 | + // We were at the last item, stay there |
| 278 | + result = selected; |
| 279 | + } |
| 280 | + } |
| 281 | + |
| 282 | + selected.removeClass( 'os-suggest-result-hl' ); |
| 283 | + result.addClass( 'os-suggest-result-hl' ); |
| 284 | + } |
| 285 | + |
| 286 | + if ( updateTextbox ) { |
| 287 | + if ( result.size() == 0 ) |
| 288 | + restoreText(); |
| 289 | + else |
| 290 | + conf._data.textbox.val( result.data( 'text' ) ); |
| 291 | + } |
| 292 | + |
| 293 | + if ( result.size() > 0 && conf._data.visibleResults < conf.suggestions.length ) { |
| 294 | + // Not all suggestions are visible |
| 295 | + // Scroll if needed |
| 296 | + |
| 297 | + // height of a result row |
| 298 | + var rowHeight = result.outerHeight(); |
| 299 | + // index of first visible element |
| 300 | + var first = conf._data.div.scrollTop() / rowHeight; |
| 301 | + // index of last visible element |
| 302 | + var last = first + conf._data.visibleResults - 1; |
| 303 | + // index of element to scroll to |
| 304 | + var to = result.attr( 'rel' ); |
| 305 | + |
| 306 | + if ( to < first ) |
| 307 | + // Need to scroll up |
| 308 | + conf._data.div.scrollTop( to * rowHeight ); |
| 309 | + else if ( result.attr( 'rel' ) > last ) |
| 310 | + // Need to scroll down |
| 311 | + conf._data.div.scrollTop( ( to - conf._data.visibleResults + 1 ) * rowHeight ); |
| 312 | + } |
| 313 | + } |
| 314 | + |
| 315 | + /** |
| 316 | + * Initialize the widget |
| 317 | + */ |
| 318 | + function init() { |
| 319 | + if ( typeof conf != 'object' || typeof conf._data != 'undefined' ) |
| 320 | + // Configuration not set or init already done |
| 321 | + return; |
| 322 | + |
| 323 | + // Set defaults |
| 324 | + if ( typeof conf.animationDuration == 'undefined' ) |
| 325 | + conf.animationDuration = 200; |
| 326 | + if ( typeof conf.cancelPending != 'function' ) |
| 327 | + conf.cancelPending = function() {}; |
| 328 | + if ( typeof conf.delay == 'undefined' ) |
| 329 | + conf.delay = 250; |
| 330 | + if ( typeof conf.maxGrowFactor == 'undefined' ) |
| 331 | + conf.maxGrowFactor = 2; |
| 332 | + if ( typeof conf.maxRows == 'undefined' ) |
| 333 | + conf.maxRows = 7; |
| 334 | + if ( typeof conf.submitOnClick == 'undefined' ) |
| 335 | + conf.submitOnClick = false; |
| 336 | + if ( typeof conf.suggestions != 'object' ) |
| 337 | + conf.suggestions = []; |
| 338 | + |
| 339 | + conf._data = {}; |
| 340 | + conf._data.textbox = $(this); |
| 341 | + conf._data.timerID = null; // ID of running timer |
| 342 | + conf._data.prevText = null; // Text in textbox when suggestions were last fetched |
| 343 | + conf._data.visibleResults = 0; // Number of results visible without scrolling |
| 344 | + conf._data.mouseDownOn = $( [] ); // Suggestion the last mousedown event occured on |
| 345 | + |
| 346 | + // Create container div for suggestions |
| 347 | + conf._data.div = $( '<div />' ) |
| 348 | + .addClass( 'os-suggest' ) //TODO: use own CSS |
| 349 | + .css( { |
| 350 | + top: Math.round( $(this).offset().top ) + this.offsetHeight, |
| 351 | + left: Math.round( $(this).offset().left ), |
| 352 | + width: $(this).outerWidth() |
| 353 | + }) |
| 354 | + .hide() |
| 355 | + .appendTo( $( 'body' ) ); |
| 356 | + |
| 357 | + // Create results table |
| 358 | + $( '<table />' ) |
| 359 | + .addClass( 'os-suggest-results' ) // TODO: use descendant selector |
| 360 | + .width( $(this).outerWidth() ) // TODO: see if we need Opera width fix |
| 361 | + .appendTo( conf._data.div ); |
| 362 | + |
| 363 | + $(this) |
| 364 | + // Stop browser autocomplete from interfering |
| 365 | + .attr( 'autocomplete', 'off') |
| 366 | + .keydown( function( e ) { |
| 367 | + // Store key pressed to handle later |
| 368 | + conf._data.keypressed = (e.keyCode == undefined) ? e.which : e.keyCode; |
| 369 | + conf._data.keypressed_count = 0; |
| 370 | + }) |
| 371 | + .keypress( function() { |
| 372 | + conf._data.keypressed_count++; |
| 373 | + processKey( conf._data.keypressed ); |
| 374 | + }) |
| 375 | + .keyup( function() { |
| 376 | + // Some browsers won't throw keypress() for |
| 377 | + // arrow keys. If we got a keydown and a keyup |
| 378 | + // without a keypress in between, solve that |
| 379 | + if (conf._data.keypressed_count == 0 ) |
| 380 | + processKey( conf._data.keypressed ); |
| 381 | + }) |
| 382 | + .blur( function() { |
| 383 | + // When losing focus because of a mousedown |
| 384 | + // on a suggestion, don't hide the suggestions |
| 385 | + if ( conf._data.mouseDownOn.size() > 0 ) |
| 386 | + return; |
| 387 | + conf._data.div.hide(); |
| 388 | + cancelPendingSuggestions(); |
| 389 | + }); |
| 390 | + |
| 391 | + conf._data.div |
| 392 | + .mouseover( function( e ) { |
| 393 | + var tr = $( e.target ).closest( '.os-suggest tr' ); |
| 394 | + highlightResult( tr, false ); |
| 395 | + }) |
| 396 | + // Can't use click() because the container div is hidden |
| 397 | + // when the textbox loses focus. Instead, listen for a |
| 398 | + // mousedown followed by a mouseup on the same <tr> |
| 399 | + .mousedown( function( e ) { |
| 400 | + var tr = $( e.target ).closest( '.os-suggest tr' ); |
| 401 | + conf._data.mouseDownOn = tr; |
| 402 | + }) |
| 403 | + .mouseup( function( e ) { |
| 404 | + var tr = $( e.target ).closest( '.os-suggest tr' ); |
| 405 | + var other = conf._data.mouseDownOn; |
| 406 | + conf._data.mouseDownOn = $( [] ); |
| 407 | + if ( tr.get( 0 ) != other.get( 0 ) ) |
| 408 | + return; |
| 409 | + |
| 410 | + highlightResult( tr, true ); |
| 411 | + conf._data.div.hide(); |
| 412 | + conf._data.textbox.focus(); |
| 413 | + if ( conf.submitOnClick ) |
| 414 | + conf._data.textbox.closest( 'form' ) |
| 415 | + .submit(); |
| 416 | + }); |
| 417 | + } |
| 418 | + |
| 419 | + function getProperty( prop ) { |
| 420 | + return ( param[0] == '_' ? undefined : conf[param] ); |
| 421 | + } |
| 422 | + |
| 423 | + function setProperty( prop, value ) { |
| 424 | + if ( typeof conf == 'undefined' ) { |
| 425 | + $(this).data( 'suggestionsConfiguration', {} ); |
| 426 | + conf = $(this).data( 'suggestionsConfiguration' ); |
| 427 | + } |
| 428 | + if ( prop[0] != '_' ) |
| 429 | + conf[prop] = value; |
| 430 | + if ( prop == 'suggestions' && conf._data ) |
| 431 | + // Setting suggestions post-init |
| 432 | + suggestionsChanged(); |
| 433 | + } |
| 434 | + |
| 435 | + |
| 436 | + // Body of suggestions() starts here |
| 437 | + var conf = $(this).data( 'suggestionsConfiguration' ); |
| 438 | + if ( typeof param == 'object' ) |
| 439 | + return this.each( function() { |
| 440 | + // Bulk-set properties |
| 441 | + for ( key in param ) { |
| 442 | + // Make sure that this in setProperty() |
| 443 | + // is set right |
| 444 | + setProperty.call( this, key, param[key] ); |
| 445 | + } |
| 446 | + }); |
| 447 | + else if ( typeof param == 'string' ) { |
| 448 | + if ( typeof param2 != 'undefined' ) |
| 449 | + return this.each( function() { |
| 450 | + setProperty( param, param2 ); |
| 451 | + }); |
| 452 | + else |
| 453 | + return getProperty( param ); |
| 454 | + } else if ( typeof param != 'undefined' ) |
| 455 | + // Incorrect usage, ignore |
| 456 | + return this; |
| 457 | + |
| 458 | + // No parameters given, initialize |
| 459 | + return this.each( init ); |
| 460 | +};})(jQuery); |
Index: trunk/phase3/js2/mwEmbed/mv_embed.js |
— | — | @@ -191,6 +191,7 @@ |
192 | 192 | "$j.secureEvalJSON" : "jquery/plugins/jquery.secureEvalJSON.js", |
193 | 193 | "$j.cookie" : "jquery/plugins/jquery.cookie.js", |
194 | 194 | "$j.contextMenu" : "jquery/plugins/jquery.contextMenu.js", |
| 195 | + "$j.fn.suggestions" : "jquery/plugins/jquery.suggestions.js", |
195 | 196 | |
196 | 197 | "$j.effects.blind" : "jquery/jquery.ui/ui/effects.blind.js", |
197 | 198 | "$j.effects.drop" : "jquery/jquery.ui/ui/effects.drop.js", |
Index: trunk/phase3/js2/ajaxcategories.js |
— | — | @@ -0,0 +1,315 @@ |
| 2 | +loadGM( { |
| 3 | + "ajax-add-category":"[Add Category]", |
| 4 | + "ajax-add-category-submit":"[Add]", |
| 5 | + "ajax-confirm-prompt":"[Confirmation Text]", |
| 6 | + "ajax-confirm-title":"[Confirmation Title]", |
| 7 | + "ajax-confirm-save":"[Save]", |
| 8 | + "ajax-add-category-summary":"[Add category $1]", |
| 9 | + "ajax-remove-category-summary":"[Remove category $2]", |
| 10 | + "ajax-confirm-actionsummary":"[Summary]", |
| 11 | + "ajax-error-title":"Error", |
| 12 | + "ajax-error-dismiss":"OK", |
| 13 | + "ajax-remove-category-error":"[RemoveErr]" |
| 14 | + } ); |
| 15 | + |
| 16 | +var ajaxCategories = { |
| 17 | + |
| 18 | + handleAddLink : function(e) { |
| 19 | + e.preventDefault(); |
| 20 | + |
| 21 | + // Make sure the suggestion plugin is loaded. Load everything else while we're at it |
| 22 | + mvJsLoader.doLoad( ['$j.ui', '$j.ui.dialog', '$j.fn.suggestions'], |
| 23 | + function() { |
| 24 | + $j('#mw-addcategory-prompt').toggle(); |
| 25 | + |
| 26 | + $j('#mw-addcategory-input').suggestions( { |
| 27 | + 'fetch':ajaxCategories.fetchSuggestions, |
| 28 | + 'cancel': function() { |
| 29 | + var req = ajaxCategories.request; |
| 30 | + if (req.abort) |
| 31 | + req.abort() |
| 32 | + }, |
| 33 | + } ); |
| 34 | + |
| 35 | + $j('#mw-addcategory-input').suggestions(); |
| 36 | + } ); |
| 37 | + }, |
| 38 | + |
| 39 | + fetchSuggestions : function( query ) { |
| 40 | + var that = this; |
| 41 | + var request = $j.ajax( { |
| 42 | + url: wgScriptPath + '/api.php', |
| 43 | + data: { |
| 44 | + 'action': 'query', |
| 45 | + 'list': 'allpages', |
| 46 | + 'apnamespace': 14, |
| 47 | + 'apprefix': $j(this).val(), |
| 48 | + 'format': 'json' |
| 49 | + }, |
| 50 | + dataType: 'json', |
| 51 | + success: function( data ) { |
| 52 | + // Process data.query.allpages into an array of titles |
| 53 | + var pages = data.query.allpages; |
| 54 | + var titleArr = []; |
| 55 | + |
| 56 | + $j.each(pages, function(i, page) { |
| 57 | + var title = page.title.split( ':', 2 )[1]; |
| 58 | + titleArr.push(title); |
| 59 | + } ); |
| 60 | + |
| 61 | + $j(that).suggestions( 'suggestions', titleArr ); |
| 62 | + } |
| 63 | + }); |
| 64 | + |
| 65 | + ajaxCategories.request = request; |
| 66 | + }, |
| 67 | + |
| 68 | + reloadCategoryList : function( response ) { |
| 69 | + var holder = $j('<div/>'); |
| 70 | + |
| 71 | + holder.load( window.location.href+' .catlinks', function() { |
| 72 | + $j('.catlinks').replaceWith( holder.find('.catlinks') ); |
| 73 | + ajaxCategories.setupAJAXCategories(); |
| 74 | + ajaxCategories.removeProgressIndicator( $j('.catlinks') ); |
| 75 | + }); |
| 76 | + }, |
| 77 | + |
| 78 | + confirmEdit : function( page, fn, actionSummary, doneFn ) { |
| 79 | + // Load jQuery UI |
| 80 | + mvJsLoader.doLoad( ['$j.ui', '$j.ui.dialog', '$j.suggestions'], function() { |
| 81 | + // Produce a confirmation dialog |
| 82 | + |
| 83 | + var dialog = $j('<div/>'); |
| 84 | + |
| 85 | + dialog.addClass('mw-ajax-confirm-dialog'); |
| 86 | + dialog.attr( 'title', gM('ajax-confirm-title') ); |
| 87 | + |
| 88 | + // Intro text. |
| 89 | + var confirmIntro = $j('<p/>'); |
| 90 | + confirmIntro.text( gM('ajax-confirm-prompt') ); |
| 91 | + dialog.append(confirmIntro); |
| 92 | + |
| 93 | + // Summary of the action to be taken |
| 94 | + var summaryHolder = $j('<p/>'); |
| 95 | + var summaryLabel = $j('<strong/>'); |
| 96 | + summaryLabel.text(gM('ajax-confirm-actionsummary')+" " ); |
| 97 | + summaryHolder.text( actionSummary ); |
| 98 | + summaryHolder.prepend( summaryLabel ); |
| 99 | + dialog.append(summaryHolder); |
| 100 | + |
| 101 | + // Reason textbox. |
| 102 | + var reasonBox = $j('<input type="text" size="45" />'); |
| 103 | + reasonBox.addClass('mw-ajax-confirm-reason'); |
| 104 | + dialog.append(reasonBox); |
| 105 | + |
| 106 | + // Submit button |
| 107 | + var submitButton = $j('<input type="button"/>'); |
| 108 | + submitButton.val( gM( 'ajax-confirm-save' ) ); |
| 109 | + |
| 110 | + var submitFunction = function() { |
| 111 | + ajaxCategories.addProgressIndicator( dialog ); |
| 112 | + ajaxCategories.doEdit( page, fn, reasonBox.val(), |
| 113 | + function() { |
| 114 | + doneFn(); |
| 115 | + dialog.dialog('close'); |
| 116 | + ajaxCategories.removeProgressIndicator( dialog ); |
| 117 | + } |
| 118 | + ); |
| 119 | + }; |
| 120 | + |
| 121 | + var buttons = {}; |
| 122 | + buttons[gM('ajax-confirm-save')] = submitFunction; |
| 123 | + var dialogOptions = { |
| 124 | + 'AutoOpen' : true, |
| 125 | + 'buttons' : buttons, |
| 126 | + 'width' : 450, |
| 127 | + }; |
| 128 | + |
| 129 | + $j('#catlinks').prepend(dialog); |
| 130 | + dialog.dialog( dialogOptions ); |
| 131 | + } ); |
| 132 | + }, |
| 133 | + |
| 134 | + doEdit : function( page, fn, summary, doneFn ) { |
| 135 | + // Get an edit token for the page. |
| 136 | + var getTokenVars = { |
| 137 | + 'action':'query', |
| 138 | + 'prop':'info|revisions', |
| 139 | + 'intoken':'edit', |
| 140 | + 'titles':page, |
| 141 | + 'rvprop':'content|timestamp', |
| 142 | + 'format':'json', |
| 143 | + }; |
| 144 | + $j.get(wgScriptPath+'/api.php', getTokenVars, |
| 145 | + function( reply ) { |
| 146 | + var infos = reply.query.pages; |
| 147 | + $j.each(infos, function(pageid, data) { |
| 148 | + var token = data.edittoken; |
| 149 | + var timestamp = data.revisions[0].timestamp; |
| 150 | + var oldText = data.revisions[0]['*']; |
| 151 | + |
| 152 | + var newText = fn(oldText); |
| 153 | + |
| 154 | + if (newText === false) return; |
| 155 | + |
| 156 | + var postEditVars = { |
| 157 | + 'action':'edit', |
| 158 | + 'title':page, |
| 159 | + 'text':newText, |
| 160 | + 'summary':summary, |
| 161 | + 'token':token, |
| 162 | + 'basetimestamp':timestamp, |
| 163 | + 'format':'json', |
| 164 | + }; |
| 165 | + |
| 166 | + $j.post( wgScriptPath+'/api.php', postEditVars, doneFn, 'json' ); |
| 167 | + } ); |
| 168 | + } |
| 169 | + , 'json' ); |
| 170 | + }, |
| 171 | + |
| 172 | + addProgressIndicator : function( elem ) { |
| 173 | + var indicator = $j('<div/>'); |
| 174 | + |
| 175 | + indicator.addClass('mw-ajax-loader'); |
| 176 | + |
| 177 | + elem.append( indicator ); |
| 178 | + }, |
| 179 | + |
| 180 | + removeProgressIndicator : function( elem ) { |
| 181 | + elem.find('.mw-ajax-loader').remove(); |
| 182 | + }, |
| 183 | + |
| 184 | + handleCategoryAdd : function(e) { |
| 185 | + // Grab category text |
| 186 | + var category = $j('#mw-addcategory-input').val(); |
| 187 | + var appendText = "\n[["+wgFormattedNamespaces[14]+":"+category+"]]\n"; |
| 188 | + var summary = gM('ajax-add-category-summary', category); |
| 189 | + |
| 190 | + ajaxCategories.confirmEdit( wgPageName, function(oldText) { return oldText+appendText }, |
| 191 | + summary, ajaxCategories.reloadCategoryList ); |
| 192 | + }, |
| 193 | + |
| 194 | + handleDeleteLink : function(e) { |
| 195 | + e.preventDefault(); |
| 196 | + |
| 197 | + var category = $j(this).parent().find('a').text(); |
| 198 | + |
| 199 | + // Build a regex that matches legal invocations of that category. |
| 200 | + |
| 201 | + // In theory I should escape the aliases, but there's no JS function for it |
| 202 | + // Shouldn't have any real impact, can't be exploited or anything, so we'll |
| 203 | + // leave it for now. |
| 204 | + var categoryNSFragment = ''; |
| 205 | + $j.each(wgNamespaceIds, function( name, id ) { |
| 206 | + if (id == 14) { |
| 207 | + // Allow the first character to be any case |
| 208 | + var firstChar = name.charAt(0); |
| 209 | + firstChar = '['+firstChar.toUpperCase()+firstChar.toLowerCase()+']' |
| 210 | + categoryNSFragment += '|'+firstChar+name.substr(1); |
| 211 | + } |
| 212 | + } ); |
| 213 | + categoryNSFragment = categoryNSFragment.substr(1) // Remove leading | |
| 214 | + |
| 215 | + |
| 216 | + // Build the regex |
| 217 | + var titleFragment = category; |
| 218 | + |
| 219 | + firstChar = category.charAt(0); |
| 220 | + firstChar = '['+firstChar.toUpperCase()+firstChar.toLowerCase()+']'; |
| 221 | + titleFragment = firstChar+category.substr(1); |
| 222 | + var categoryRegex = '\\[\\['+categoryNSFragment+':'+titleFragment+'(\\|[^\\]]*)?\\]\\]'; |
| 223 | + categoryRegex = new RegExp( categoryRegex, 'g' ); |
| 224 | + |
| 225 | + var summary = gM('ajax-remove-category-summary', category); |
| 226 | + |
| 227 | + ajaxCategories.confirmEdit( wgPageName, |
| 228 | + function(oldText) { |
| 229 | + var newText = oldText.replace(categoryRegex, ''); |
| 230 | + |
| 231 | + if (newText == oldText) { |
| 232 | + var error = gM('ajax-remove-category-error'); |
| 233 | + ajaxCategories.showError( error ); |
| 234 | + ajaxCategories.removeProgressIndicator( $j('.mw-ajax-confirm-dialog') ); |
| 235 | + $j('.mw-ajax-confirm-dialog').dialog('close'); |
| 236 | + return false; |
| 237 | + } |
| 238 | + |
| 239 | + return newText; |
| 240 | + }, summary, ajaxCategories.reloadCategoryList ); |
| 241 | + }, |
| 242 | + |
| 243 | + showError : function( str ) { |
| 244 | + var dialog = $j('<div/>'); |
| 245 | + dialog.text(str); |
| 246 | + |
| 247 | + $j('#bodyContent').append(dialog); |
| 248 | + |
| 249 | + var buttons = {}; |
| 250 | + buttons[gM('ajax-error-dismiss')] = function(e) { dialog.dialog('close'); }; |
| 251 | + var dialogOptions = { |
| 252 | + 'buttons' : buttons, |
| 253 | + 'AutoOpen' : true, |
| 254 | + 'title' : gM('ajax-error-title'), |
| 255 | + }; |
| 256 | + |
| 257 | + dialog.dialog(dialogOptions); |
| 258 | + }, |
| 259 | + |
| 260 | + setupAJAXCategories : function() { |
| 261 | + var clElement = $j('.catlinks'); |
| 262 | + |
| 263 | + // Unhide hidden category holders. |
| 264 | + clElement.removeClass( 'catlinks-allhidden' ); |
| 265 | + |
| 266 | + var addLink = $j('<a/>'); |
| 267 | + addLink.addClass( 'mw-ajax-addcategory' ); |
| 268 | + |
| 269 | + // Create [Add Category] link |
| 270 | + addLink.text( gM( 'ajax-add-category' ) ); |
| 271 | + addLink.attr('href', '#'); |
| 272 | + addLink.click( ajaxCategories.handleAddLink ); |
| 273 | + clElement.append(addLink); |
| 274 | + |
| 275 | + // Create add category prompt |
| 276 | + var promptContainer = $j('<div id="mw-addcategory-prompt"/>'); |
| 277 | + var promptTextbox = $j('<input type="text" size="45" id="mw-addcategory-input"/>'); |
| 278 | + var addButton = $j('<input type="button" id="mw-addcategory-button"/>' ); |
| 279 | + addButton.val( gM('ajax-add-category-submit') ); |
| 280 | + |
| 281 | + promptTextbox.keypress( ajaxCategories.handleCategoryInput ); |
| 282 | + addButton.click( ajaxCategories.handleCategoryAdd ); |
| 283 | + |
| 284 | + promptContainer.append(promptTextbox); |
| 285 | + promptContainer.append(addButton); |
| 286 | + promptContainer.hide(); |
| 287 | + |
| 288 | + // Create delete link for each category. |
| 289 | + $j('.catlinks div span a').each( function(e) { |
| 290 | + // Create a remove link |
| 291 | + var deleteLink = $j('<a class="mw-remove-category" href="#"/>'); |
| 292 | + |
| 293 | + deleteLink.click(ajaxCategories.handleDeleteLink); |
| 294 | + |
| 295 | + $j(this).after(deleteLink); |
| 296 | + } ); |
| 297 | + |
| 298 | + clElement.append(promptContainer); |
| 299 | + }, |
| 300 | + |
| 301 | +}; |
| 302 | + |
| 303 | +js2AddOnloadHook( ajaxCategories.setupAJAXCategories ); |
| 304 | +loadGM( { |
| 305 | + "ajax-add-category":"[Add Category]", |
| 306 | + "ajax-add-category-submit":"[Add]", |
| 307 | + "ajax-confirm-prompt":"[Confirmation Text]", |
| 308 | + "ajax-confirm-title":"[Confirmation Title]", |
| 309 | + "ajax-confirm-save":"[Save]", |
| 310 | + "ajax-add-category-summary":"[Add category $1]", |
| 311 | + "ajax-remove-category-summary":"[Remove category $2]", |
| 312 | + "ajax-confirm-actionsummary":"[Summary]", |
| 313 | + "ajax-error-title":"Error", |
| 314 | + "ajax-error-dismiss":"OK", |
| 315 | + "ajax-remove-category-error":"[RemoveErr]" |
| 316 | + } ); |
Index: trunk/phase3/languages/messages/MessagesEn.php |
— | — | @@ -4194,4 +4194,18 @@ |
4195 | 4195 | 'htmlform-reset' => 'Undo changes', |
4196 | 4196 | 'htmlform-selectorother-other' => 'Other', |
4197 | 4197 | |
| 4198 | +'ajax-add-category' => 'Add Category', |
| 4199 | +'ajax-add-category-submit' => 'Add', |
| 4200 | +'ajax-confirm-title' => 'Confirm Action', |
| 4201 | +'ajax-confirm-prompt' => 'Please confirm this action, and enter the reason for it in the |
| 4202 | +box below. Once you are happy to submit it, click "Save". Note that repeatedly making false |
| 4203 | +edits will result in your being blocked from Wikipedia.', |
| 4204 | +'ajax-confirm-save' => 'Save', |
| 4205 | +'ajax-add-category-summary' => 'Add category "$1"', |
| 4206 | +'ajax-remove-category-summary' => 'Remove category "$1"', |
| 4207 | +'ajax-confirm-actionsummary' => 'Action to take:', |
| 4208 | +'ajax-error-title' => 'Error', |
| 4209 | +'ajax-error-dismiss' => 'OK', |
| 4210 | +'ajax-remove-category-error' => 'It was not possible to remove this category. This usually |
| 4211 | +occurs when the category has been added to the page in a template.', |
4198 | 4212 | ); |