Index: trunk/extensions/UploadWizard/UploadWizardHooks.php |
— | — | @@ -37,6 +37,7 @@ |
38 | 38 | 'resources/jquery/jquery.autoEllipsis.js', |
39 | 39 | 'resources/jquery/jquery.suggestions.js', |
40 | 40 | 'resources/jquery/jquery.removeCtrl.js', |
| 41 | + 'resources/jquery/jquery.pubsub.js', |
41 | 42 | |
42 | 43 | // mediawiki-specific interface helper (relies on mediawiki globals) |
43 | 44 | 'resources/jquery/jquery.mwCoolCats.js', |
Index: trunk/extensions/UploadWizard/resources/jquery/jquery.pubsub.js |
— | — | @@ -0,0 +1,61 @@ |
| 2 | +/** |
| 3 | + * Minimal pubsub framework |
| 4 | + * |
| 5 | + * Loosely based on https://github.com/phiggins42/bloody-jquery-plugins/pubsub.js, which is itself BSD-licensed. |
| 6 | + */ |
| 7 | + |
| 8 | +( function( $ ) { |
| 9 | + /** |
| 10 | + * Store of events -> array of listener callbacks |
| 11 | + */ |
| 12 | + var subs = {}; |
| 13 | + |
| 14 | + /** |
| 15 | + * Publish an event |
| 16 | + * Additional variadic arguments after the event name are passed as arguments to the subscriber functions |
| 17 | + * @param {String} name of event |
| 18 | + * @return {Number} number of subscribers |
| 19 | + */ |
| 20 | + $.publish = function( name /* , args... */ ) { |
| 21 | + var args = [].slice.call( arguments, 1 ); |
| 22 | + $.each( subs[name], function( i, sub ) { |
| 23 | + sub.apply( null, args ); |
| 24 | + } ); |
| 25 | + return subs[name].length; |
| 26 | + }; |
| 27 | + |
| 28 | + /** |
| 29 | + * Subscribe to an event. |
| 30 | + * @param {String} name of event to listen for |
| 31 | + * @param {Function} callback to run when event occurs |
| 32 | + * @return {Array} returns handle which can be used as argument to unsubscribe() |
| 33 | + */ |
| 34 | + $.subscribe = function( name, fn ) { |
| 35 | + if (!subs[name]) { |
| 36 | + subs[name] = []; |
| 37 | + } |
| 38 | + subs[name].push(fn); |
| 39 | + return [ name, fn ]; |
| 40 | + }; |
| 41 | + |
| 42 | + /** |
| 43 | + * Given the handle of a particular subscription, remove it |
| 44 | + * @param {Array} object returned by subscribe ( array of event name and callback ) |
| 45 | + * @return {Boolean} success |
| 46 | + */ |
| 47 | + $.unsubscribe = function( nameFn ) { |
| 48 | + var name = nameFn[0]; |
| 49 | + var fn = nameFn[1]; |
| 50 | + var success = false; |
| 51 | + if ( subs[name].length ) { |
| 52 | + $.each( subs[name], function( i, fni ) { |
| 53 | + if ( fni === fn ) { |
| 54 | + subs[name].splice( i, 1 ); |
| 55 | + success = true; |
| 56 | + return false; /* early exit loop */ |
| 57 | + } |
| 58 | + } ); |
| 59 | + } |
| 60 | + return success; |
| 61 | + }; |
| 62 | +} )( jQuery ); |
Property changes on: trunk/extensions/UploadWizard/resources/jquery/jquery.pubsub.js |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 63 | + native |
Index: trunk/extensions/UploadWizard/resources/mw.UploadWizard.js |
— | — | @@ -11,6 +11,7 @@ |
12 | 12 | this.api = api; |
13 | 13 | this.state = 'new'; |
14 | 14 | this.thumbnails = {}; |
| 15 | + this.thumbnailPublishers = {}; |
15 | 16 | this.imageinfo = {}; |
16 | 17 | this.title = undefined; |
17 | 18 | this.mimetype = undefined; |
— | — | @@ -29,10 +30,15 @@ |
30 | 31 | // this.handler = new ( mw.UploadWizard.config[ 'uploadHandlerClass' ] )( this ); |
31 | 32 | // this.handler = new mw.MockUploadHandler( this ); |
32 | 33 | this.handler = new mw.ApiUploadHandler( this, api ); |
| 34 | + |
| 35 | + this.index = mw.UploadWizardUpload.prototype.count++; |
33 | 36 | }; |
34 | 37 | |
35 | 38 | mw.UploadWizardUpload.prototype = { |
36 | 39 | |
| 40 | + // increments with each upload |
| 41 | + count: 0, |
| 42 | + |
37 | 43 | acceptDeed: function( deed ) { |
38 | 44 | var _this = this; |
39 | 45 | _this.deed.applyDeed( _this ); |
— | — | @@ -390,34 +396,87 @@ |
391 | 397 | if ( mw.isEmpty( height ) ) { |
392 | 398 | height = -1; |
393 | 399 | } |
394 | | - var key = "width" + width + ',height' + height; |
| 400 | + // this key is overspecified for this thumbnail ( we don't need to reiterate the index ) but |
| 401 | + // we can use this as key as an event now, that might fire much later |
| 402 | + var key = 'thumbnail.' + _this.index + '.width' + width + ',height' + height; |
395 | 403 | if ( mw.isDefined( _this.thumbnails[key] ) ) { |
396 | 404 | callback( _this.thumbnails[key] ); |
397 | | - } else { |
398 | | - var apiCallback = function( thumbnails ) { |
| 405 | + return; |
| 406 | + } |
| 407 | + |
| 408 | + // subscribe to the event that this thumbnail is ready -- will give null or an Image to the callback |
| 409 | + $j.subscribe( key, callback ); |
| 410 | + |
| 411 | + // if someone else already started a thumbnail fetch & publish, then don't bother, just wait for the event. |
| 412 | + if ( ! mw.isDefined( _this.thumbnailPublishers[ key ] ) ) { |
| 413 | + // The thumbnail publisher accepts the result of a stashImageInfo, and then tries to get the thumbnail, and eventually |
| 414 | + // will trigger the event we just subscribed to. |
| 415 | + _this.thumbnailPublishers[ key ] = _this.getThumbnailPublisher( key ); |
| 416 | + _this.getStashImageInfo( _this.thumbnailPublishers[ key ], [ 'url' ], width, height ); |
| 417 | + } |
| 418 | + }, |
| 419 | + |
| 420 | + /** |
| 421 | + * Returns a callback that can be used with a stashImageInfo call to fetch images, and then fire off an event to |
| 422 | + * let everyone else know the image is loaded. |
| 423 | + * |
| 424 | + * Will retry the thumbnail URL several times, as thumbnails are known to be slow in production. |
| 425 | + * @param {String} name of event to publish when thumbnails received (or final failure) |
| 426 | + */ |
| 427 | + getThumbnailPublisher: function( key ) { |
| 428 | + var _this = this; |
| 429 | + return function( thumbnails ) { |
399 | 430 | if ( thumbnails === null ) { |
400 | | - callback( null ); |
| 431 | + // the api call failed somehow, no thumbnail data. |
| 432 | + $j.publish( key, null ); |
401 | 433 | } else { |
402 | | - for ( var i = 0; i < thumbnails.length; i++ ) { |
403 | | - var thumb = thumbnails[i]; |
| 434 | + // ok, the api callback has returned us information on where the thumbnail(s) ARE, but that doesn't mean |
| 435 | + // they are actually there yet. Keep trying to set the source ( which should trigger "error" or "load" event ) |
| 436 | + // on the image. If it loads publish the event with the image. If it errors out too many times, give up and publish |
| 437 | + // the event with a null. |
| 438 | + $j.each( thumbnails, function( i, thumb ) { |
404 | 439 | if ( thumb.thumberror || ( ! ( thumb.thumburl && thumb.thumbwidth && thumb.thumbheight ) ) ) { |
405 | 440 | mw.log( "mw.UploadWizardUpload::getThumbnail> thumbnail error or missing information" ); |
406 | | - callback( null ); |
| 441 | + $j.publish( key, null ); |
407 | 442 | return; |
408 | 443 | } |
| 444 | + |
| 445 | + // try to load this image with exponential backoff |
| 446 | + // if the delay goes past 8 seconds, it gives up and publishes the event with null |
| 447 | + var timeoutMs = 100; |
| 448 | + |
409 | 449 | var image = document.createElement( 'img' ); |
410 | | - $j( image ).load( function() { |
411 | | - callback( image ); |
412 | | - } ); |
413 | 450 | image.width = thumb.thumbwidth; |
414 | 451 | image.height = thumb.thumbheight; |
415 | | - image.src = thumb.thumburl; |
| 452 | + $j( image ) |
| 453 | + .load( function() { |
| 454 | + // cache this thumbnail |
416 | 455 | _this.thumbnails[key] = image; |
| 456 | + // publish the image to anyone who wanted it |
| 457 | + $j.publish( key, image ); |
| 458 | + } ) |
| 459 | + .error( function() { |
| 460 | + // retry with exponential backoff |
| 461 | + if ( timeoutMs < 8000 ) { |
| 462 | + setTimeout( function() { |
| 463 | + timeoutMs = timeoutMs * 2 + Math.round( Math.random() * ( timeoutMs / 10 ) ); |
| 464 | + setSrc(); |
| 465 | + }, timeoutMs ) |
| 466 | + } else { |
| 467 | + $j.publish( key, null ); |
417 | 468 | } |
418 | | - } |
| 469 | + } ); |
| 470 | + |
| 471 | + // executing this should cause a .load() or .error() event on the image |
| 472 | + function setSrc() { |
| 473 | + image.src = thumb.thumburl; |
419 | 474 | }; |
420 | | - _this.getStashImageInfo( apiCallback, [ 'url' ], width, height ); |
| 475 | + |
| 476 | + // and, go! |
| 477 | + setSrc(); |
| 478 | + } ); |
421 | 479 | } |
| 480 | + }; |
422 | 481 | }, |
423 | 482 | |
424 | 483 | /** |
— | — | @@ -877,10 +936,6 @@ |
878 | 937 | |
879 | 938 | _this.uploads.push( upload ); |
880 | 939 | |
881 | | - /* useful for making ids unique and so on */ |
882 | | - _this.uploadsSeen++; |
883 | | - upload.index = _this.uploadsSeen; |
884 | | - |
885 | 940 | _this.updateFileCounts(); |
886 | 941 | |
887 | 942 | upload.deedPreview = new mw.UploadWizardDeedPreview( upload ); |
— | — | @@ -889,9 +944,6 @@ |
890 | 945 | upload.details = new mw.UploadWizardDetails( upload, $j( '#mwe-upwiz-macro-files' ) ); |
891 | 946 | }, |
892 | 947 | |
893 | | - /* increments with every upload */ |
894 | | - uploadsSeen: 0, |
895 | | - |
896 | 948 | /** |
897 | 949 | * Remove an upload from our array of uploads, and the HTML UI |
898 | 950 | * We can remove the HTML UI directly, as jquery will just get the parent. |