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

Change subject: Add lazy loading and image rows
......................................................................

Add lazy loading and image rows

Lazily load images for the image result bowser. Display images in rows of 
varying height, with
every elment in a row of the same height. This prevents staggaring at the 
bottom and makes the
order of images clear. Images load in parallel and rows display as soon as 
every image in the
row loads.

Bug: T166216
Change-Id: I8447b28c0db0015a257879dc77c1a74c44f3c62c
---
M embed.html
M index.html
M style.less
M wikibase/queryService/ui/ResultView.js
M wikibase/queryService/ui/resultBrowser/ImageResultBrowser.js
A wikibase/tests/queryService/ui/resultBrowser/ImageResultBrowser.test.js
M wikibase/tests/queryService/ui/resultBrowser/ResultBrowser.test.js
7 files changed, 337 insertions(+), 72 deletions(-)


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

diff --git a/embed.html b/embed.html
index 708a3f2..bdb66bc 100644
--- a/embed.html
+++ b/embed.html
@@ -108,7 +108,10 @@
 .header-toolbar:hover {
        opacity: 1;
 }
-
+.img-grid {
+       width: 95vw;
+}
+       
 </style>
 <body>
        <div class="logo"></div>
@@ -130,7 +133,6 @@
                                        </a>
                                                <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>
diff --git a/index.html b/index.html
index 031dbd4..ac585a4 100644
--- a/index.html
+++ b/index.html
@@ -260,6 +260,12 @@
                <div class="row">
                        <div id="query-error" class="panel-heading">Test 
error</div>
                </div>
+               <div class="row">
+                       <div id="loading-spinner">
+                               <i class="fa fa-spinner fa-pulse fa-3x 
fa-fw"></i>
+                               <span class="sr-only">Loading...</span>
+                       </div>
+               </div>
 
                <div class="explorer-panel panel panel-default">
                    <div class="panel-heading clearfix">
diff --git a/style.less b/style.less
index a743b1b..90fe29a 100644
--- a/style.less
+++ b/style.less
@@ -394,75 +394,64 @@
        color: rgba( 51, 122, 183, 0.45 );
 }
 
-/* masonry */
-.masonry {
+/* image grid */
+.img-grid {
        width: 95%;
        margin: 3em auto;
-       margin: 1.5em 0;
+       margin: 1.5em auto;
        padding: 0;
-       -moz-column-gap: 1.5em;
-       -webkit-column-gap: 1.5em;
-       column-gap: 1.5em;
        font-size: 0.85em;
 }
-.item > a > img {
+.item.hidden {
+       visibility: hidden;
+}
+.item-row {
        width: 100%;
-       display: inline-block;
+}
+.hidden-row {
+       height: 50px;
+       visibility: hidden;
 }
 .item {
-       display: inline-block;
        background: #fff;
        padding: 1em;
-       margin: 0 0 1.5em;
-       width: 100%;
+       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;
+}
+.hidden-row>.item{
+       display: none;
+}
+.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;
 }
 
-@media only screen and ( min-width: 400px ) {
-       .masonry {
-               -moz-column-count: 2;
-               -webkit-column-count: 2;
-               column-count: 2;
-       }
+/* loading spinner */
+#loading-spinner {
+       display: none;
+       color: #777;
+}
+#loading-spinner>.fa-spinner {
+       margin: 0 auto 20px;
+       display: block;
 }
 
-@media only screen and ( min-width: 700px ) {
-       .masonry {
-               -moz-column-count: 3;
-               -webkit-column-count: 3;
-               column-count: 3;
-       }
-}
-
-@media only screen and ( min-width: 900px ) {
-       .masonry {
-               -moz-column-count: 4;
-               -webkit-column-count: 4;
-               column-count: 4;
-       }
-}
-
-@media only screen and ( min-width: 1100px ) {
-       .masonry {
-               -moz-column-count: 5;
-               -webkit-column-count: 5;
-               column-count: 5;
-       }
-}
-
-@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/ResultView.js 
b/wikibase/queryService/ui/ResultView.js
index 8332078..c12b012 100644
--- a/wikibase/queryService/ui/ResultView.js
+++ b/wikibase/queryService/ui/ResultView.js
@@ -477,7 +477,9 @@
         */
        SELF.prototype._drawResult = function( resultBrowser ) {
                var self = this;
-
+               
+               $( window ).off( 'scroll.resultBrowser' );
+               $( window ).off( 'resize.resultBrowser' );
                this._actionBar.show( 'wdqs-action-render', '',  'success', 100 
);
                window.setTimeout( function() {
                        try {
diff --git a/wikibase/queryService/ui/resultBrowser/ImageResultBrowser.js 
b/wikibase/queryService/ui/resultBrowser/ImageResultBrowser.js
index 0fcd2b7..1b86f7f 100644
--- a/wikibase/queryService/ui/resultBrowser/ImageResultBrowser.js
+++ b/wikibase/queryService/ui/resultBrowser/ImageResultBrowser.js
@@ -3,9 +3,9 @@
 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
         *
@@ -16,6 +16,7 @@
         * @constructor
         */
        function SELF() {
+               this._queue = [];
        }
 
        SELF.prototype = new 
wikibase.queryService.ui.resultBrowser.AbstractResultBrowser();
@@ -25,7 +26,43 @@
         * @private
         */
        SELF.prototype._grid = null;
-
+       
+       /**
+        * @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;
+       
+       /**
+        * the total width of the grid
+        * @private
+        */
+       SELF.prototype._totalWidth = 0;
+       
+       /**
+        * the height of each line of summary on an item
+        * @private
+        */
+       SELF.prototype._lineHeight = 0;
+       
        /**
         * Draw browser to the given element
         *
@@ -33,21 +70,218 @@
         */
        SELF.prototype.draw = function( $element ) {
                var self = this;
-               this._grid = $( '<div class="masonry">' );
-
+               //Queue which must be cleared
+               this._queue.splice( 0, this._queue.length );
+               this._grid = ( $( '<div class="img-grid">' ).html( '<div 
class="item-row hidden-row">' ) );
+               
+               $element.html( this._grid );
+               this._lineHeight = 1.5 * parseFloat( this._grid.css( 
'font-size' ) );
                this._iterateResult( function( field, key, row ) {
                        if ( field && self._isCommonsResource( field.value ) ) {
-                               var url = field.value,
-                                       fileName = 
self._getFormatter().getCommonsResourceFileName( url );
-
-                               self._grid.append( self._getItem( 
self._getThumbnail( url ), self._getThumbnail(
-                                               url, 1000 ), fileName, row ) );
+                               row.url = field.value;
+                               self._queue.push( row );
                        }
                } );
-
-               $element.html( this._grid );
+               this._fixedItemWidth = this._calculateBaseWidth();
+               this._gridWidth = this._grid.width();
+               this._lazyLoad();
        };
+       
+       /**
+        * calculate the width of an elment without content
+        */
+       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;
+       };
+       
+       /**
+        * initiate lazy loading
+        */
+       SELF.prototype._lazyLoad = function() {
+               var self = this;
+               
+               $( window ).off( 'scroll.resultBrowser' );
+               $( window ).off( 'resize.resultBrowser' );
+               if( this._queue.length ) {
+                       if ( this._getPosFromTop() < 3 * window.innerHeight ) {
+                               this._loading.show();
+                               this._loadNextChunk().done( function() { 
self._lazyLoad.call(self); } );
+                       }
+                       else  {
+                               $( window ).on( 'scroll.resultBrowser', $.proxy 
( _.debounce( self._lazyLoad, 100 ), self ) );
+                               this._loading.hide();
+                       }
+               }
+               else {
+                       this._showFinalRow();
+                       this._loading.hide();
+               }
+               $( window ).on( 'resize.resultBrowser', ( $.proxy( _.debounce( 
this._layoutPage, 100 ), self ) ) );
+       };
+       
+       /**
+        * show the last row even if it's not full
+        */
+       SELF.prototype._showFinalRow = function( hidden ) {
+               var $row = $('.item-row').last(),
+                       $items = $row.find( '.item' ),
+                       calculatedDimensions = { height: 0, widths: [] };
 
+               if ( $row.children().length ) {
+                       calculatedDimensions.height = this._heightThreshold;
+                       calculatedDimensions.widths = $items.map( function() {
+                               return $( this ).data( 'aspectRatio' ) * ( 
calculatedDimensions.height - $( this ).data( 'fixedHeight' ) );
+                       } ).toArray();
+                       this._setDimensions( $row, calculatedDimensions, hidden 
);
+               }
+               else {
+                       $row.remove();
+               }
+       };
+       
+       /**
+        * load the next block of 8 images to allow for parallel loading
+        */
+       SELF.prototype._loadNextChunk = function() {
+               var self = this,
+                       preloadNum = 8,
+                       items = this._queue.splice( 0, preloadNum ),
+                       itemsLoaded = [ $.when() ];
+               
+                       items.forEach( function( item, i) {
+                               var previousItem = itemsLoaded[i],
+                                       currentItem = $.when( 
self._preloadItem( item ), previousItem );
+                               
+                               itemsLoaded.push( currentItem );
+                               currentItem.done( function( item ) { 
self._appendItem( item ); } );
+                       } );
+               
+               return itemsLoaded[ items.length ];
+               
+       };
+       
+       /**
+        * 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._calculateDimensions = function( $row ) {
+               var $items = $row.find( '.item' ),
+                       fixedWidth = this._fixedItemWidth * $items.length,
+                       totalWidth = this._gridWidth - fixedWidth,
+                       aspectRatioSum = 0,
+                       productSum = 0,
+                       calculatedDimensions = { height: 0, widths: [] };
+               
+               $items.each( function() {
+                       var aspectRatio = $( this ).data( 'aspectRatio' ),
+                               fixedHeight = $( this ).data( 'fixedHeight' );
+                       
+                       aspectRatioSum += aspectRatio;
+                       productSum += aspectRatio * fixedHeight;
+               } );
+               
+               calculatedDimensions.height = ( totalWidth + productSum ) / 
aspectRatioSum;
+               calculatedDimensions.widths = $items.map( function() {
+                       var width = $( this ).data( 'aspectRatio' ) * ( 
calculatedDimensions.height - $( this ).data( 'fixedHeight' ) );
+                       return Math.trunc( width * 100 ) / 100;
+               } ).toArray();
+               return calculatedDimensions;
+       };
+       
+       /**
+        * lay out the page again
+        */
+       SELF.prototype._layoutPage = function() {
+               var self = this,
+                       $items = $( '.item' );
+               
+               this._gridWidth = this._grid.width();
+               $( '.item' ).unwrap();
+               this._grid.append( $( '<div class="item-row hidden-row">' ) );
+               $items.each( $.proxy( function( int, elem ) { this._appendItem( 
elem, true ); }, self ) );
+               if ( this._queue.length ) {
+                       $( '.hidden-row' ).not( '.hidden-row:last' 
).removeClass( 'hidden-row' );
+               }
+               else {
+                       this._showFinalRow( true );
+                       $( '.hidden-row' ).removeClass( 'hidden-row' );
+               }
+       };
+       
+       /**
+        * append an item to the final row and calls a function to recalculate 
the dimensions of that row
+        */
+       SELF.prototype._appendItem = function( $item, hidden ) {
+               var $currentRow = $( '.item-row' ).last();
+               $currentRow.append( $item );
+               this._layOutRow( $currentRow, hidden );
+       };
+       
+       /**
+        * return a promise which resolves with the item when the image is 
loaded
+        */
+       SELF.prototype._preloadItem = function( itemData ) {
+               var self = this,
+                       itemLoaded = $.Deferred(),
+                       url = this._getThumbnail( itemData.url, 1000 ),
+                       fileName = 
this._getFormatter().getCommonsResourceFileName( url ),
+                       item = this._getItem( this._getThumbnail( url ), 
this._getThumbnail( url, 1000 ), fileName, itemData ),
+                       fixedHeight = this._lineHeight * item.find( '.summary' 
)[ 0 ].childElementCount,
+                       img = item.find( '.item-img' );
+                       
+                       img[ 0 ].onerror = function() { img.attr( 'src', 
'https://upload.wikimedia.org/wikipedia/commons/a/ac/No_image_available.svg' ); 
};
+                       img[ 0 ].onload = function() {
+                               var aspectRatio = ( this.naturalWidth / 
this.naturalHeight );
+                               itemLoaded.resolveWith( self, item.data( 
'aspectRatio', aspectRatio ) );
+                       };
+                       item.data( 'fixedHeight', fixedHeight );
+                       img.attr( 'src', url );
+               
+               return itemLoaded.promise();
+       };
+               
+       /**
+        * lay out the passed row
+        */
+       SELF.prototype._layOutRow = function( $currentRow, hidden ) {
+               var calculatedDimensions = this._calculateDimensions( 
$currentRow );
+               
+               if ( calculatedDimensions.height < this._heightThreshold || 
Math.min( calculatedDimensions.widths ) < this._widthThreshold ) {
+                       this._setDimensions( $currentRow, calculatedDimensions, 
hidden );
+                       this._grid.append( $( '<div class="item-row 
hidden-row">' ) );
+               }
+       };
+       
+       /**
+
+        * set the dimensions of items within a row
+        */
+       SELF.prototype._setDimensions = function( $currentRow, 
calculatedDimensions, hidden ) {
+               var $items = $currentRow.find('.item');
+               
+               $items.width( function(index) { return 
calculatedDimensions.widths[ index ]; } );
+               if( !hidden ) {
+                       $currentRow.removeClass( 'hidden-row' );        
+               }
+       };
+       
        /**
         * @private
         */
@@ -55,12 +289,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
         */
@@ -98,4 +332,4 @@
        };
 
        return SELF;
-}( jQuery ) );
+}( jQuery, _ ) );
diff --git 
a/wikibase/tests/queryService/ui/resultBrowser/ImageResultBrowser.test.js 
b/wikibase/tests/queryService/ui/resultBrowser/ImageResultBrowser.test.js
new file mode 100644
index 0000000..2b093f9
--- /dev/null
+++ b/wikibase/tests/queryService/ui/resultBrowser/ImageResultBrowser.test.js
@@ -0,0 +1,32 @@
+( function( $, QUnit, sinon, wb ) {
+       'use strict';
+
+       QUnit.module( 'wikibase.queryService.ui.resultBrowser' );
+       var irb = new wb.queryService.ui.resultBrowser.ImageResultBrowser(),
+       sampleItem = '<a 
href=\"https://commons.wikimedia.org/wiki/Special:FilePath/%D0%9D%D0%B0%D1%82%D0%B0%D0%BB%D0%B8%D1%8F%20%D0%A3%D1%88%D0%B0%D0%BA%D0%BE%D0%B2%D0%B0.%201927.jpg?width=1000\";
 data-gallery=\"g\" 
data-title=\"https://commons.wikimedia.org/wiki/Special:FilePath/Наталия 
Ушакова. 1927.jpg?width=1000\"><img class=\"item-img\" 
src=\"https://commons.wikimedia.org/wiki/Special:FilePath/%D0%9D%D0%B0%D1%82%D0%B0%D0%BB%D0%B8%D1%8F%20%D0%A3%D1%88%D0%B0%D0%BA%D0%BE%D0%B2%D0%B0.%201927.jpg?width=1000\";></a><div
 class=\"summary\"><div><span title=\"url\"></span></div><div><span><a 
title=\"Show Gallery\" 
href=\"https://commons.wikimedia.org/wiki/Special:FilePath/%D0%9D%D0%B0%D1%82%D0%B0%D0%BB%D0%B8%D1%8F%20%D0%A3%D1%88%D0%B0%D0%BA%D0%BE%D0%B2%D0%B0.%201927.jpg?width=900\";
 aria-hidden=\"true\" class=\"gallery glyphicon glyphicon-picture\" 
data-gallery=\"G_pic\" data-title=\"Наталия Ушакова. 1927.jpg\"></a> <a 
title=\"pic\" href=\"https://commons.wikimedia.org/wiki/File:Наталия Ушакова. 
1927.jpg\" target=\"_blank\" class=\"item-link\" 
rel=\"noopener\">commons:Наталия Ушакова. 
1927.jpg</a></span></div><div><span><a 
href=\"http://www.wikidata.org/entity/Q28665865\"; title=\"Explore item\" 
class=\"explore glyphicon glyphicon-search\" tabindex=\"-1\" 
aria-hidden=\"true\"></a> <a title=\"item\" 
href=\"http://www.wikidata.org/entity/Q28665865\"; target=\"_blank\" 
class=\"item-link\" rel=\"noopener\">Мyka</a></span></div></div>',
+       sampleItemData = { 
+               item: { type: 'uri', value: 
'http://www.wikidata.org/entity/Q28665865' },
+               itemLabel: {
+                       'xml:lang': 'en',
+                       type: 'literal',
+                       value: 'Мyka'
+               },
+               pic: {
+                       type: 'uri',
+                       value: 
'http://commons.wikimedia.org/wiki/Special:FilePath/%D0%9D%D0%B0%D1%82%D0%B0%D0%BB%D0%B8%D1%8F%20%D0%A3%D1%88%D0%B0%D0%BA%D0%BE%D0%B2%D0%B0.%201927.jpg'
+               },
+               url: 
'http://commons.wikimedia.org/wiki/Special:FilePath/%D0%9D%D0%B0%D1%82%D0%B0%D0%BB%D0%B8%D1%8F%20%D0%A3%D1%88%D0%B0%D0%BA%D0%BE%D0%B2%D0%B0.%201927.jpg'
+       },
+               aspectRatio = 0.7558823529411764;
+
+       QUnit.test( 'When preloading an item the item should be returned', 
function( assert ) {
+               assert.expect( 2 );
+               var check = function( item ) {
+                       assert.equal( item.innerHTML , sampleItem );
+                       assert.equal( $(item).data( 'aspectRatio' ), 
aspectRatio );
+               };
+               return irb._preloadItem( sampleItemData ).done( function(x) {
+  check(x);
+} );
+       } );
+}( jQuery, QUnit, sinon, wikibase ) );
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/404402
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I8447b28c0db0015a257879dc77c1a74c44f3c62c
Gerrit-PatchSet: 1
Gerrit-Project: wikidata/query/gui
Gerrit-Branch: master
Gerrit-Owner: StudentSydney <[email protected]>

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

Reply via email to