Matthias Mullie has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/404614 )

Change subject: Display placeholder text until 3D thumb is loaded
......................................................................

Display placeholder text until 3D thumb is loaded

Bug: T183310
Change-Id: I69af9fe914690cee7c7437da0b32c480be9c8a64
---
M extension.json
M i18n/en.json
M i18n/qqq.json
M modules/ext.3d.js
M modules/mmv.3d.head.js
5 files changed, 154 insertions(+), 14 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/3D 
refs/changes/14/404614/1

diff --git a/extension.json b/extension.json
index adea519..0982c8f 100644
--- a/extension.json
+++ b/extension.json
@@ -30,7 +30,11 @@
                                "ext.3d.less"
                        ],
                        "messages": [
-                               "3d-badge-text"
+                               "3d-badge-text",
+                               "3d-thumb-placeholder"
+                       ],
+                       "dependencies": [
+                               "jquery.spinner"
                        ]
                },
                "mmv.3d": {
diff --git a/i18n/en.json b/i18n/en.json
index ae681da..50cf79e 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -6,5 +6,6 @@
        },
        "3d": "3d",
        "3d-desc": "Provides support for 3d file formats",
-       "3d-badge-text": "3D"
+       "3d-badge-text": "3D",
+       "3d-thumb-placeholder": "Loading thumbnail..."
 }
diff --git a/i18n/qqq.json b/i18n/qqq.json
index c6937a2..d72bbcc 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -8,5 +8,6 @@
        },
        "3d": "{{name}}",
        "3d-desc": 
"{{desc|name=3d|url=https://www.mediawiki.org/wiki/Extension:3d}}\n\n[[wikipedia:3D
 computer graphics|Read more about 3D]]",
-       "3d-badge-text": "{{optional}}\nText for the badge shown in the top 
left corner of images to identify 3D files"
+       "3d-badge-text": "{{optional}}\nText for the badge shown in the top 
left corner of images to identify 3D files",
+       "3d-thumb-placeholder": "Placeholder text to display while the 
thumbnail is still loading"
 }
diff --git a/modules/ext.3d.js b/modules/ext.3d.js
index bfb53c3..7a6315c 100644
--- a/modules/ext.3d.js
+++ b/modules/ext.3d.js
@@ -19,26 +19,158 @@
        'use strict';
 
        mw.threed = {
-               wrap: function ( $element ) {
-                       if ( !$element.parent().hasClass( 'mw-3d-wrapper' ) ) {
-                               $element.wrap( $( '<span>' ).addClass( 
'mw-3d-wrapper' ) );
-                       }
+               /**
+                * @type {object}
+                */
+               thumbnailPromises: {},
 
-                       return $element.parent();
+               /**
+                * @param {jQuery} $elements
+                */
+               wrap: function ( $elements ) {
+                       $elements.each( function ( i, element ) {
+                               if ( !$( element ).parent().hasClass( 
'mw-3d-wrapper' ) ) {
+                                       $( element ).wrap( $( '<span>' 
).addClass( 'mw-3d-wrapper' ) );
+                               }
+                       } );
+
+                       return $elements.parent();
                },
 
                /**
-                * @param {jQuery} $element
+                * @param {jQuery} $elements
                 */
-               attachBadge: function ( $element ) {
-                       var $wrap = this.wrap( $element ),
+               attachBadge: function ( $elements ) {
+                       var $wrap = this.wrap( $elements ),
                                $badge = $( '<span>' )
                                        .addClass( 'mw-3d-badge' )
                                        .text( mw.message( '3d-badge-text' 
).text() );
 
-                       $wrap.append( $badge );
+                       $elements.each( function ( i, element ) {
+                               this.thumbnailLoadComplete( element ).then( 
function () { $wrap.append( $badge ) } );
+                       }.bind( this ) );
+               },
+
+               /**
+                * @param {jQuery} $elements
+                */
+               addThumbnailPlaceholder: function ( $elements ) {
+                       var $spinner = $.createSpinner( { size: 'small', type: 
'inline' } ),
+                               $placeholder = $( '<p>' )
+                                       .addClass( 'mw-3d-thumb-placeholder' )
+                                       .text( ' ' + mw.message( 
'3d-thumb-placeholder' ).text() + ' ' )
+                                       .prepend( $spinner );
+
+                       // hide the image and put a placeholder there instead
+                       $elements.hide().after( $placeholder );
+
+                       $elements.each( function( i, element ) {
+                               this.thumbnailLoadComplete( element )
+                                       .then( function ( element ) {
+                                               // image confirmed to have 
loaded: show it & remove placeholder
+                                               $( element ).siblings( 
'.mw-3d-thumb-placeholder' ).remove();
+                                               $( element ).show();
+                                       } );
+                       }.bind( this ) );
+               },
+
+               /**
+                * Figure out if the thumbnail has completed loading.
+                *
+                * This is different from imageLoadComplete in that that one 
will just check
+                * for an HTMLImageElement to have completed loading, whereas 
this method
+                * will continuously attempt to load the image (even after it 
has failed to
+                * load the first time around) until it succeeds, at which 
point it will
+                * replace the original HTMLImageElement with the one where 
loading succeeded.
+                *
+                * @param {HTMLImageElement} img
+                * @return {$.Promise} Promise that resolves with the thumbnail 
HTMLImageElement
+                */
+               thumbnailLoadComplete: function ( img ) {
+                       // safeguard to prevent the thumbnail node cloning 
below from being executed
+                       // more than once...
+                       if ( img.src in this.thumbnailPromises ) {
+                               return this.thumbnailPromises[ img.src ];
+                       }
+
+                       // I could nest this.imageLoadComplete's returned 
promises, but if it takes
+                       // forever to load the image, we'd keep filling up the 
call stack to the
+                       // point that it could crash. Instead, I'll create a 
new deferred object
+                       // that'll resolve once we've found the thumbnail 
loading to be complete.
+                       var deferred = $.Deferred(),
+                               reload = function () {
+                                       // cloning the img element will trigger 
the browser to load the
+                                       // image again, after which we can 
simply redo this procedure,
+                                       // until the image has completed loading
+                                       var $clone = $( img ).clone();
+                                       this.imageLoadComplete( $clone.get( 0 ) 
).then(
+                                               function () {
+                                                       $( img ).replaceWith( 
$clone );
+                                                       deferred.resolve( 
$clone.get( 0 ) );
+                                               },
+                                               function() {
+                                                       // wait 5 seconds 
before attempting to load the image again
+                                                       setTimeout( reload, 
5000 );
+                                               }
+                                       );
+                               }.bind( this );
+
+                       this.thumbnailPromises[ img.src ] = deferred.promise();
+
+                       reload();
+
+                       return this.thumbnailPromises[ img.src ];
+               },
+
+               /**
+                * Figure out image status by returning a promise that resolves 
when the
+                * images had loaded successfully, or rejects when it errored.
+                *
+                * We can't figure out the http status code of <img> elements 
to test
+                * if it succeeded to load. Instead, let's first check if the 
image
+                * has completed loading at all, then check the naturalHeight of
+                * the image - if it's non-zero, we know that the thumb exists;
+                * otherwise, it's obviously not there & we'll want to display a
+                * placeholder & periodically retry loading it.
+                * naturalHeight is safe to use on all supported browsers:
+                *
+                * @see 
https://www.mediawiki.org/wiki/Compatibility#Browser_support_matrix
+                * @see https://caniuse.com/#feat=img-naturalwidth-naturalheight
+                *
+                * @param {HTMLImageElement} img
+                * @return {$.Promise} resolves with HTMLImageElement when/once 
image has
+                *   loaded successfully; rejects if it failed
+                * @private
+                */
+               imageLoadComplete: function ( img ) {
+                       var deferred = $.Deferred(),
+                               func,
+                               interval;
+
+                       func = function() {
+                               if ( !img.complete ) {
+                                       return;
+                               }
+
+                               clearInterval( interval );
+
+                               if ( img.naturalHeight === 0 ) {
+                                       deferred.reject();
+                               } else {
+                                       deferred.resolve( img );
+                               }
+                       };
+
+                       // schedule interval, but also test it right away; it 
may have loaded
+                       // already...
+                       interval = setInterval( func, 100 );
+                       func();
+
+                       return deferred.promise();
                }
        };
 
-       mw.threed.attachBadge( $( 'img[src$=".stl.png"]' ) );
+       var $thumbs = $( 'img[src$=".stl.png"]' );
+       mw.threed.attachBadge( $thumbs );
+       mw.threed.addThumbnailPlaceholder( $thumbs );
 }( mediaWiki, jQuery ) );
diff --git a/modules/mmv.3d.head.js b/modules/mmv.3d.head.js
index 8fb6823..85dbfb8 100644
--- a/modules/mmv.3d.head.js
+++ b/modules/mmv.3d.head.js
@@ -44,7 +44,9 @@
                        view.on( 'click', this.open.bind( this, $image, $link ) 
);
                        download.on( 'click', this.download.bind( this, $link ) 
);
 
-                       $wrap.append( $buttonWrap );
+                       $image.each( function ( i, element ) {
+                               mw.threed.thumbnailLoadComplete( element 
).then( function () { $wrap.append( $buttonWrap ) } );
+                       }.bind( this ) );
 
                        // clicking file should open it in MMV instead of 
prompting download
                        $link.on( 'click', function ( e ) {

-- 
To view, visit https://gerrit.wikimedia.org/r/404614
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I69af9fe914690cee7c7437da0b32c480be9c8a64
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/3D
Gerrit-Branch: master
Gerrit-Owner: Matthias Mullie <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to