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