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