StudentSydney has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/401897 )

Change subject: Add lazy loading of images in query results and display in rows
......................................................................

Add lazy loading of images in query results and display in rows

Load 8 images at a time in parallel, and continue loading well below the 
current window. Display
images in rows, each row full of items of a uniorm height within the row and 
all rows of uniform
width. The current display doesn't load lazily and doesn't shpw the images in 
the propper order.
I tried a masonry diaplay but it is too akward to have the final row totally 
staggard when
images are of unusual heights.

Bug: T166216
Change-Id: I0cf79007fd6cbb728c593b233c84222a32c95242
---
M embed.html
M index.html
M style.less
M wikibase/queryService/ui/resultBrowser/ImageResultBrowser.js
M wikibase/tests/index.html
M wikibase/tests/queryService/ui/resultBrowser/ResultBrowser.test.js
6 files changed, 283 insertions(+), 129 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/wikidata/query/gui 
refs/changes/97/401897/1

diff --git a/embed.html b/embed.html
index 72cbf8a..70ea199 100644
--- a/embed.html
+++ b/embed.html
@@ -6,6 +6,12 @@
 <meta name="viewport" content="width=device-width, initial-scale=1.0, 
user-scalable=yes">
 <title>Wikidata Query Service</title>
 
+<!-- build:none -->
+<link rel="stylesheet/less" type="text/css" href="style.less">
+<script src="node_modules/less/dist/less.js" data-env="development"></script>
+<script>less.watch()</script>
+<!-- endbuild -->
+       
 <!-- build:css css/embed.style.min.css -->
 <link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.css">
 <link rel="stylesheet" href="node_modules/font-awesome/css/font-awesome.css">
@@ -21,11 +27,6 @@
 <link rel="stylesheet" 
href="node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css">
 <link rel="stylesheet" 
href="node_modules/jstree/dist/themes/default/style.css" />
 <link rel="stylesheet" href="style.css">
-<!-- endbuild -->
-<!-- build:none -->
-<link rel="stylesheet/less" type="text/css" href="style.less">
-<script src="node_modules/less/dist/less.js" data-env="development"></script>
-<script>less.watch()</script>
 <!-- endbuild -->
 
 <link rel="shortcut icon" href="favicon.ico">
@@ -131,7 +132,6 @@
                                                <ul id="result-browser-menu" 
class="dropdown-menu" role="menu">
                                                </ul></li>
                                        <li>
-                                       <li>
                                                <a target="_blank" class="help" 
rel="noopener" 
href="https://www.wikidata.org/wiki/Wikidata:SPARQL_query_service/Wikidata_Query_Help/Result_Views";>
                                                <span class="fa 
fa-question-circle"></span></a>
                                        </li>
@@ -148,8 +148,12 @@
                        <div class="message"></div>
                </div>
                <div id="query-result">Test result</div>
-               <div id="query-error" class="panel-heading">Test error</div>
 
+               <div id="query-error" class="panel-heading">Test error</div>
+               <div id="loading-spinner">
+                       <i class="fa fa-spinner fa-pulse fa-3x fa-fw"></i>
+                       <span class="sr-only">Loading...</span>
+               </div>
                <div class="explorer-panel panel panel-default">
                    <div class="panel-heading clearfix">
                      <h1 class="panel-title pull-left" style="padding-top: 
7.5px;">Explorer</h1>
diff --git a/index.html b/index.html
index ad0a129..9c252bb 100644
--- a/index.html
+++ b/index.html
@@ -6,6 +6,13 @@
        <meta name="viewport" content="width=device-width, initial-scale=1.0, 
user-scalable=yes">
        <title>Wikidata Query Service</title>
 
+       
+       <!-- build:none -->
+       <link rel="stylesheet/less" type="text/css" href="style.less">
+       <script src="node_modules/less/dist/less.js" 
data-env="development"></script>
+       <script>less.watch()</script>
+       <!-- endbuild -->
+       
     <!-- build:css css/style.min.css -->
        <link rel="stylesheet" 
href="node_modules/bootstrap/dist/css/bootstrap.css">
        <link rel="stylesheet" 
href="node_modules/bootstrap/dist/css/bootstrap-theme.css">
@@ -33,11 +40,6 @@
        <link rel="stylesheet" href="style.css">
        <!-- endbuild -->
 
-       <!-- build:none -->
-       <link rel="stylesheet/less" type="text/css" href="style.less">
-       <script src="node_modules/less/dist/less.js" 
data-env="development"></script>
-       <script>less.watch()</script>
-       <!-- endbuild -->
 
        <link rel="shortcut icon" href="favicon.ico">
        <!-- build:js js/shim.min.js -->
@@ -312,7 +314,6 @@
                        </div>
                </div>
        </div>
-
        <!-- JS files -->
        <!-- build:js js/vendor.min.js -->
        <script src="node_modules/jquery/dist/jquery.js"></script>
@@ -368,7 +369,6 @@
        <script src="vendor/bootstrap-tags/js/bootstrap-tags.min.js"></script>
        <script src="vendor/sparqljs/dist/sparqljs-browser-min.js"></script>
        <script 
src="vendor/bootstrapx-clickover/bootstrapx-clickover.js"></script>
-       <script 
src="node_modules/masonry-layout/dist/masonry.pkgd.min.js"></script>
        <!-- endbuild -->
 
        <!-- build:js js/wdqs.min.js -->
diff --git a/style.less b/style.less
index da2b69a..42bd246 100644
--- a/style.less
+++ b/style.less
@@ -405,14 +405,51 @@
        color: rgba( 51, 122, 183, 0.45 );
 }
 
-/* masonry */
-.masonry {
+/* image grid */
+.img-grid {
        width: 95%;
        margin: 3em auto;
        margin: 1.5em auto;
        padding: 0;
        font-size: 0.85em;
 }
+.item.hidden {
+       visibility: hidden;
+}
+.item-row {
+       width: 100%;
+}
+.hidden-row {
+       height: 50px;
+       visibility: hidden;
+}
+.item {
+       background: #fff;
+       padding: 1em;
+       margin: 0 0.75em 1.5em;
+       box-sizing: border-box;
+       -moz-box-sizing: border-box;
+       -webkit-box-sizing: border-box;
+       box-shadow: 2px 2px 4px 0 #ccc;
+       display: inline-block;
+}
+.item-img {
+       width: 100%;
+}
+.summary>div {
+       height: 1.5em;
+}
+.summary>div>span {
+       white-space: nowrap;
+    text-overflow: ellipsis;
+    display: block;
+    overflow: hidden;
+}
+.summary .glyphicon {
+       display: inline;
+}
+
+/* loading spinner */
 #loading-spinner {
        display: none;
        color: #777;
@@ -421,54 +458,8 @@
        margin: 0 auto 20px;
        display: block;
 }
-.item {
-       display: inline-block;
-       background: #fff;
-       padding: 1em;
-       margin: 0 0 1.5em;
-       width: 20%;
-       box-sizing: border-box;
-       -moz-box-sizing: border-box;
-       -webkit-box-sizing: border-box;
-       box-shadow: 2px 2px 4px 0 #ccc;
-       visibility: hidden;
-}
-.item>a>img {
-       width: 100%;
-}
 
-@media only screen and ( min-width: 400px ) {
-       .item {
-               width: ~"calc( 50% - 10px )";
-       }
-}
-
-@media only screen and ( min-width: 700px ) {
-       .item {
-               width: ~"calc( 33.33% - 10px )";
-       }
-}
-
-@media only screen and ( min-width: 900px ) {
-       .item {
-               width: ~"calc( 25% - 10px )";
-       }
-}
-
-@media only screen and ( min-width: 1100px ) {
-       .item {
-               width: ~"calc( 20% - 10px )";
-       }
-}
-@media only screen and ( min-width: 1280px ) {
-       .wrapper {
-               width: 1260px;
-       }
-}
-/*
-       ActionBar
-*/
-
+/* ActionBar */
 .action-bar .progress {
        height: 30px;
        font-size: 30px;
diff --git a/wikibase/queryService/ui/resultBrowser/ImageResultBrowser.js 
b/wikibase/queryService/ui/resultBrowser/ImageResultBrowser.js
index 670815d..20a5edc 100644
--- a/wikibase/queryService/ui/resultBrowser/ImageResultBrowser.js
+++ b/wikibase/queryService/ui/resultBrowser/ImageResultBrowser.js
@@ -1,10 +1,12 @@
+/* jshint esversion: 6 */
 var wikibase = wikibase || {};
 wikibase.queryService = wikibase.queryService || {};
 wikibase.queryService.ui = wikibase.queryService.ui || {};
 wikibase.queryService.ui.resultBrowser = 
wikibase.queryService.ui.resultBrowser || {};
 
-wikibase.queryService.ui.resultBrowser.ImageResultBrowser = ( function( $ ) {
+wikibase.queryService.ui.resultBrowser.ImageResultBrowser = ( function( $, _ ) 
{
        'use strict';
+       
        /**
         * A result browser for images
         *
@@ -24,87 +26,243 @@
         * @private
         */
        SELF.prototype._grid = null;
+       
+       /**
+        * an array of objects with items with images which have not yet been 
loaded, along with their src values
+        * @private
+        */
+       SELF.prototype._queue = [];
 
+       
+       /**
+        * @property {jQuery}
+        * @private
+        */
+       SELF.prototype._loading = $( '#loading-spinner' );
+       
+       /**
+        * the maximum height of items on the grid
+        * @private
+        */
+       SELF.prototype._heightThreshold = 400;
+       
+       /**
+        * used to determine the minimum width of items on the grid
+        * @private
+        */
+       SELF.prototype._widthThreshold = 70;
+       
+       
+       /**
+        * the portion of the width of each item which is fixed (border, 
margin, etc.)
+        * @private
+        */
+       SELF.prototype._fixedItemWidth = 0;
+       
        /**
         * Draw browser to the given element
         *
         * @param {jQuery} $element to draw at
-        */     
+        */
        SELF.prototype.draw = function( $element ) {
                var self = this;
-               SELF.prototype._grid = $( '<div class="masonry">' );
-               $element.html( this._grid );
-               SELF.prototype._grid.masonry( {
-                       itemSelector: '.item',
-                       columnWidth: '.item',
-                       gutter: 10,
-                       horizontalOrder: true
-               } );
                
-               var urls=[];
-               var rows=[];
+               this._grid = $( '<div class="img-grid">' ).append( $( '<div 
class="item-row hidden-row">' ) );
+               $element.html( this._grid );
                this._iterateResult( function( field, key, row ) {
                        if ( field && self._isCommonsResource( field.value ) ) {
-                               urls.push( field.value );
-                               rows.push( row );
+                               var url = field.value,
+                                       fileName = 
self._getFormatter().getCommonsResourceFileName( url ),
+                                       item = self._getItem( 
self._getThumbnail( url ), self._getThumbnail(
+                                               url, 1000 ), fileName, row ),
+                                       queueItem = { item: item, url: url };
+                               
+                               self._queue.push( queueItem );
                        }
                } );
-               SELF.prototype.lazyLoad( urls, rows );
+               this._fixedItemWidth = this._calculateBaseWidth();
+               this._lazyLoad();
        };
        
        /**
-        * @private
+        * calculate the width of an elment without content
         */
-       SELF.prototype.lazyLoad = function( urls, rows ) {
-                       var count = 1,
-                               $spinner = $( '#loading-spinner' ),
-                               lazyLoadCheck,
-                               loadMore = function() {
-                                       clearInterval( lazyLoadCheck );
-                                       if( count < urls.length ) {
-                                               var posFromTop = $( '.item' 
).last().offset().top - $( window ).scrollTop();
-                                               if( posFromTop < 
window.innerHeight + 200 ) {
-                                                       $spinner.show();
-                                                       appendItem();
-                                               }
-                                               else {
-                                                       $spinner.hide();
-                                                       setLazyLoad();
-                                               }
-                                       }
-                                       else { $spinner.hide(); }
-                                       },
-                               appendItem = function() {
-                                       var currentItem = 
SELF.prototype.createItem( urls[count], rows[count] );
-                                       count++;
-                                       $( currentItem ).find('img').one( 
'load', function(){
-                                               currentItem.css( 'visibility', 
'visible' );
-                                               SELF.prototype._grid.masonry( 
'appended', currentItem );
-                                               loadMore();
-                                       } );
-                                       SELF.prototype._grid.append( 
currentItem ) ;
-                               },
-                               // simpler than throttling a scroll event
-                               setLazyLoad = function() { lazyLoadCheck = 
setInterval( loadMore, 100 ); },
-                       // need to do 1st item sightly differentlty so masonry 
can learn how wide to make columns
-                               firstItem = SELF.prototype.createItem( urls[0], 
rows[0] );
-                       $(firstItem).find('img').one( 'load', function(){
-                               SELF.prototype._grid.masonry();
-                               loadMore();
-                       } );
-                       firstItem.css( 'visibility', 'visible' );
-                       SELF.prototype._grid.append(firstItem).masonry( 
'appended', firstItem );
-               };
+       SELF.prototype._calculateBaseWidth = function() {
+               var baseWidth = 0,
+                       components = [ 'margin-left', 'margin-right', 
'padding-left', 'padding-right' ],
+                       $element = $( '<div class="item hidden">' );
+               
+               this._grid.append( $element );
+               components.forEach( function( component ) {
+                       baseWidth +=  parseFloat( $element.css( component ) );
+               });
+               $element.remove();
+               
+               return baseWidth;
+       };
        
        /**
-        * @private
+        * initiate lazy loading
         */
-       SELF.prototype.createItem = function ( url, row ){
-               var fileName = this._getFormatter().getCommonsResourceFileName( 
url ),
-               mainImg = this._getThumbnail( url, 1000 );
-               return( this._getItem( this._getThumbnail( url ), mainImg, 
fileName, row ) );
+       SELF.prototype._lazyLoad = function() {
+               var self = this;
+               
+               $( window ).off( 'scroll.imgResultBrowser' );
+               $( window ).off( 'resize.imgResultBrowser' );
+               if( this._queue.length ) {
+                       if ( this._getPosFromTop() < 3 * window.innerHeight ) {
+                               this._loading.show();
+                               this._loadNextChunk().then( function() { 
self._lazyLoad.call(self); } );
+                       }
+                       else  {
+                               $( window ).on( 'scroll.imgResultBrowser', 
$.proxy ( _.debounce( this._lazyLoad, 100), self ) );
+                               this._loading.hide();
+                       }
+               }
+               else {
+                       this._showFinalRow();
+                       this._loading.hide();
+               }
+               $( window ).on( 'resize.imgResultBrowser', ( $.proxy( 
_.debounce( this._layoutPage, 100), self ) ) );
        };
+       
+       /**
+        * show the last row even if it's not full
+        */
+       SELF.prototype._showFinalRow = function() {
+               var $row = $('.item-row').last(),
+                       imgs,
+                       summaries,
+                       lineHeight,
+                       aspectRatios,
+                       fixedHeights,
+                       calculatedDimensions = { height: 0, widths: [] };
+               
+               if ( $row.children().length ) {
+                       imgs = $row.find('.item-img').toArray();
+                       summaries = $row.find('.summary').toArray();
+                       lineHeight = 1.5 * parseFloat( this._grid.css( 
'font-size' ) );
+                       aspectRatios = imgs.map( img => img.naturalWidth / 
img.naturalHeight );
+                       fixedHeights = summaries.map( ( summary ) => 
summary.childElementCount * lineHeight );
+                       calculatedDimensions.height = this._heightThreshold;
+                       calculatedDimensions.widths = aspectRatios.map( ( 
ratio, index ) => ratio * ( calculatedDimensions.height - fixedHeights[ index ] 
) );
 
+                       this._setDimensions( $row, calculatedDimensions );
+               }
+               else {
+                       $row.remove();
+               }
+       };
+       
+       /**
+        * load the next block of 8 images to allow for parallel loading
+        */
+       SELF.prototype._loadNextChunk = async function() {
+               var items = this._queue.splice( 0, 8 ),
+                       itemsLoaded = items.map( obj => this._preloadImg( 
obj.item, obj.url ) );
+               
+                       for(var i=0; i< Math.min( 8, items.length ); i++) {
+                               await itemsLoaded[ i ];
+                               this._appendItem( items[ i ].item );
+                       }
+
+               return Promise.all( itemsLoaded );
+               
+       };
+       
+       /**
+        * return the distance from the final row of loaded imaged to the 
bottom of the window
+        */
+       SELF.prototype._getPosFromTop = function() {
+               var lastRow = $( '.item-row' ).last();
+               return lastRow.offset().top - $( window ).scrollTop();
+       };
+       
+       /**
+        * calculate the dimensions of items wthin a row
+        */
+       SELF.prototype._calculateHeight = function( $row ) {
+               var totalItems = $row.children().length,
+                       fixedWidth = this._fixedItemWidth * totalItems,
+                       totalWidth = this._grid.width() - fixedWidth,
+                       imgs = $row.find('.item-img').toArray(),
+                       summaries = $row.find('.summary').toArray(),
+                       lineHeight = 1.5 * parseFloat( this._grid.css( 
'font-size' ) ),
+                       aspectRatios = imgs.map( img => img.naturalWidth / 
img.naturalHeight ),
+                       fixedHeights = summaries.map( ( summary ) => 
summary.childElementCount * lineHeight ),
+                       productSum = 0,
+                       calculatedDimensions = { height: 0, widths: [] };
+                       
+               aspectRatios.forEach( function( aspectRatio, index ) { 
+                       productSum += aspectRatio * fixedHeights[ index ];
+               } );
+               try {
+                       calculatedDimensions.height = ( totalWidth + productSum 
) / aspectRatios.reduce( ( sum, currentValue ) => sum + currentValue);
+               }
+               catch ( e ) {
+                       this.prototype._layoutPage();
+               }
+               calculatedDimensions.widths = aspectRatios.map( ( ratio, index 
) => ratio * ( calculatedDimensions.height - fixedHeights[ index ] ) );
+               
+               return calculatedDimensions;
+       };
+       
+       /**
+        * lay out the page again
+        */
+       SELF.prototype._layoutPage = function() {
+               var $items = $( '.item' );
+               $( '.item-row' ).remove();
+               this._grid.append( $( '<div class="item-row">' ) );
+               var self = this;
+               $items.each( $.proxy( ( int, elem ) => this._appendItem( elem 
), self ) );
+               this._showFinalRow();
+       };
+       
+       /**
+        * append an item to the final row and calls a function to recalculate 
the dimensions of that row
+        */
+       SELF.prototype._appendItem = function( $item ) {
+               var $currentRow = $( '.item-row' ).last();
+               $currentRow.append( $item );
+               this._layOutRow( $currentRow );
+       };
+       
+       /**
+        * return a promise which resolves with the image when the image is 
loaded
+        */
+       SELF.prototype._preloadImg = function( item, url ) {
+               return new Promise( function ( resolve ) {
+                       var $image = item.find( '.item-img' );
+                       
+                       $image[0].onload = () => resolve( $image );
+                       $image[0].onerror = () => resolve( $image );
+                       $image.attr( 'src', url );
+               });
+       };
+               
+       /**
+        * lay out the passed row
+        */
+       SELF.prototype._layOutRow = function( $currentRow ) {
+               var calculatedDimensions = this._calculateHeight( $currentRow );
+               
+               if ( calculatedDimensions.height < this._heightThreshold || 
Math.min( calculatedDimensions.widths ) < this._widthThreshold ) {
+                       this._setDimensions( $currentRow, calculatedDimensions 
);
+                       this._grid.append( $( '<div class="item-row 
hidden-row">' ) );
+               }
+       };
+       
+       /**
+        * set the dimensions of items within a row
+        */
+       SELF.prototype._setDimensions = function( $currentRow, 
calculatedDimensions ) {
+               var $items = $currentRow.find('.item');
+               
+               $items.width( (index) => calculatedDimensions.widths[ index ] );
+               $currentRow.removeClass( 'hidden-row' );
+       };
+       
        /**
         * @private
         */
@@ -112,12 +270,12 @@
                var $image = $( '<a>' )
                                .click( 
this._getFormatter().handleCommonResourceItem )
                                .attr( { href: url, 'data-gallery': 'g', 
'data-title': title } )
-                               .append( $( '<img>' ).attr( { 'src': 
thumbnailUrl } ) ),
-                       $summary = this._getFormatter().formatRow( row );
+                               .append( $( '<img class="item-img" >' ) ),
+                       $summary = this._getFormatter().formatRow( row 
).addClass( 'summary' );
 
                return $( '<div class="item">' ).append( $image, $summary );
        };
-
+       
        /**
         * @private
         */
@@ -155,4 +313,4 @@
        };
 
        return SELF;
-}( jQuery ) );
+}( jQuery, _ ) );
diff --git a/wikibase/tests/index.html b/wikibase/tests/index.html
index 53d43c9..89a1e70 100644
--- a/wikibase/tests/index.html
+++ b/wikibase/tests/index.html
@@ -71,6 +71,7 @@
        <script 
src="queryService/ui/resultBrowser/helper/Options.test.js"></script>
        <script 
src="queryService/ui/resultBrowser/ResultBrowser.test.js"></script>
        <script 
src="queryService/ui/resultBrowser/CoordinateResultBrowser.test.js"></script>
+       <script 
src="queryService/ui/resultBrowser/ImageResultBrowser.test.js"></script>
        <script src="queryService/api/CodeSamples.test.js"></script>
 </body>
 </html>
diff --git a/wikibase/tests/queryService/ui/resultBrowser/ResultBrowser.test.js 
b/wikibase/tests/queryService/ui/resultBrowser/ResultBrowser.test.js
index 5e5d84b..322cc1d 100644
--- a/wikibase/tests/queryService/ui/resultBrowser/ResultBrowser.test.js
+++ b/wikibase/tests/queryService/ui/resultBrowser/ResultBrowser.test.js
@@ -31,7 +31,7 @@
 
        var expected = {
                TableResultBrowser: '<div class="bootstrap-table">',
-               ImageResultBrowser: '<div class="masonry">',
+               ImageResultBrowser: '<div class="img-grid">',
                CoordinateResultBrowser: '<div id="map" 
.*class="leaflet-container',
                BubbleChartResultBrowser: '<svg',
                LineChartResultBrowser: '<svg',

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: I0cf79007fd6cbb728c593b233c84222a32c95242
Gerrit-PatchSet: 1
Gerrit-Project: wikidata/query/gui
Gerrit-Branch: master
Gerrit-Owner: StudentSydney <sydv...@gmail.com>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to