mediawiki-extensions-Visual.../modules/ve-mw/ui/widgets/ve.ui.MWMediaSearchWidget.js
Moriel Schottlender f0f8f52080 Refactor MWMediaResultWidget to resize better
For the masonry view, the result widget resizes itself according to
the rows and other images in the row. Up until now the resize was
a bit buggy, occasionally missing height/width values. This is
mostly due to rounding errors, but also due to a small double
calculation that is now fixed with this code.

Change-Id: If18adc5280e4bdbb9174b7c7e6e4eadf3c1ab26d
2015-02-26 16:42:14 -08:00

392 lines
9.6 KiB
JavaScript

/*!
* VisualEditor UserInterface MWMediaSearchWidget class.
*
* @copyright 2011-2015 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Creates an ve.ui.MWMediaSearchWidget object.
*
* @class
* @extends OO.ui.SearchWidget
*
* @constructor
* @param {Object} [config] Configuration options
* @param {number} [size] Vertical size of thumbnails
*/
ve.ui.MWMediaSearchWidget = function VeUiMWMediaSearchWidget( config ) {
// Configuration initialization
config = ve.extendObject( {
placeholder: ve.msg( 'visualeditor-media-input-placeholder' )
}, config );
// Parent constructor
OO.ui.SearchWidget.call( this, config );
// Properties
this.providers = {};
this.searchValue = '';
this.resourceQueue = new ve.dm.MWMediaResourceQueue( {
limit: 20,
threshold: 10
} );
this.queryTimeout = null;
this.itemCache = {};
this.promises = [];
this.lang = config.lang || 'en';
this.$panels = config.$panels;
// Masonry fit properties
this.rows = [];
this.rowHeight = config.rowHeight || 200;
this.queryMediaQueueCallback = this.queryMediaQueue.bind( this );
this.layoutQueue = [];
this.numItems = 0;
this.selected = null;
this.noItemsMessage = new OO.ui.LabelWidget( {
$: this.$,
label: ve.msg( 'visualeditor-dialog-media-noresults' ),
classes: [ 've-ui-mwMediaSearchWidget-noresults' ]
} );
this.noItemsMessage.toggle( false );
// Events
this.$results.on( 'scroll', this.onResultsScroll.bind( this ) );
this.$query.append( this.noItemsMessage.$element );
this.results.connect( this, {
choose: 'onResultsChoose',
add: 'onResultsAdd',
remove: 'onResultsRemove'
} );
// Initialization
this.$element.addClass( 've-ui-mwMediaSearchWidget' );
};
/* Inheritance */
OO.inheritClass( ve.ui.MWMediaSearchWidget, OO.ui.SearchWidget );
/* Methods */
/**
* Query all sources for media.
*
* @method
*/
ve.ui.MWMediaSearchWidget.prototype.queryMediaQueue = function () {
var search = this,
value = this.query.getValue();
if ( value === '' ) {
return;
}
this.query.pushPending();
search.noItemsMessage.toggle( false );
this.resourceQueue.setParams( { gsrsearch: value } );
this.resourceQueue.get( 20 )
.then( function ( items ) {
if ( items.length > 0 ) {
search.processQueueResults( items, value );
}
search.query.popPending();
search.noItemsMessage.toggle( search.results.getItems().length === 0 );
if ( search.results.getItems().length > 0 ) {
search.lazyLoadResults();
}
} );
};
/**
* Process the media queue giving more items
*
* @method
* @param {Object[]} items Given items by the media queue
*/
ve.ui.MWMediaSearchWidget.prototype.processQueueResults = function ( items ) {
var i, len, title,
resultWidgets = [],
inputSearchQuery = this.query.getValue(),
queueSearchQuery = this.resourceQueue.getSearchQuery();
if ( inputSearchQuery === '' || queueSearchQuery !== inputSearchQuery ) {
return;
}
for ( i = 0, len = items.length; i < len; i++ ) {
title = new mw.Title( items[i].title ).getMainText();
// Do not insert duplicates
if ( !Object.prototype.hasOwnProperty.call( this.itemCache, title ) ) {
this.itemCache[title] = true;
resultWidgets.push(
new ve.ui.MWMediaResultWidget( {
$: this.$,
data: items[i],
rowHeight: this.rowHeight,
maxWidth: this.results.$element.width() / 3,
minWidth: 30,
rowWidth: this.results.$element.width()
} )
);
}
}
this.results.addItems( resultWidgets );
};
/**
* Handle search value change
*
* @param {string} value New value
*/
ve.ui.MWMediaSearchWidget.prototype.onQueryChange = function ( value ) {
var trimmed = $.trim( value );
if ( trimmed === this.searchValue ) {
return;
}
this.searchValue = trimmed;
// Parent method
OO.ui.SearchWidget.prototype.onQueryChange.apply( this, arguments );
// Reset
this.itemCache = {};
this.resetRows();
// Empty the results queue
this.layoutQueue = [];
// Change resource queue query
this.resourceQueue.setParams( { gsrsearch: this.searchValue } );
// Queue
clearTimeout( this.queryTimeout );
this.queryTimeout = setTimeout( this.queryMediaQueueCallback, 350 );
};
/**
* Respond to choosing result event.
*
* @param {OO.ui.OptionWidget} item Selected item
*/
ve.ui.MWMediaSearchWidget.prototype.onResultsChoose = function ( item ) {
this.emit( 'choose', item.getData() );
};
/**
* Handle results scroll events.
*
* @param {jQuery.Event} e Scroll event
*/
ve.ui.MWMediaSearchWidget.prototype.onResultsScroll = function () {
var position = this.$results.scrollTop() + this.$results.outerHeight(),
threshold = this.results.$element.outerHeight() - this.rowHeight * 3;
// Check if we need to ask for more results
if ( !this.query.isPending() && position > threshold ) {
this.queryMediaQueue();
}
this.lazyLoadResults();
};
/**
* Lazy-load the images that are visible.
*/
ve.ui.MWMediaSearchWidget.prototype.lazyLoadResults = function () {
var i, elementTop,
items = this.results.getItems(),
resultsScrollTop = this.$results.scrollTop(),
position = resultsScrollTop + this.$results.outerHeight();
// Lazy-load results
for ( i = 0; i < items.length; i++ ) {
elementTop = items[i].$element.position().top;
if ( elementTop <= position && !items[i].hasSrc() ) {
// Load the image
items[i].lazyLoad();
}
}
};
/**
* Reset all the rows; destroy the jQuery elements and reset
* the rows array.
*/
ve.ui.MWMediaSearchWidget.prototype.resetRows = function () {
var i, len;
for ( i = 0, len = this.rows.length; i < len; i++ ) {
this.rows[i].$element.remove();
}
this.rows = [];
};
/**
* Find an available row at the end. Either we will need to create a new
* row or use the last available row if it isn't full.
*
* @return {number} Row index
*/
ve.ui.MWMediaSearchWidget.prototype.getAvailableRow = function () {
var row;
if ( this.rows.length === 0 ) {
row = 0;
} else {
row = this.rows.length - 1;
}
if ( !this.rows[row] ) {
// Create new row
this.rows[row] = {
isFull: false,
width: 0,
items: [],
$element: this.$( '<div>' )
.addClass( 've-ui-mwMediaResultWidget-row' )
.css( {
overflow: 'hidden'
} )
.data( 'row', row )
.attr( 'data-full', false )
};
// Append to results
this.results.$element.append( this.rows[row].$element );
} else if ( this.rows[row].isFull ) {
row++;
// Create new row
this.rows[row] = {
isFull: false,
width: 0,
items: [],
$element: this.$( '<div>' )
.addClass( 've-ui-mwMediaResultWidget-row' )
.css( {
overflow: 'hidden'
} )
.data( 'row', row )
.attr( 'data-full', false )
};
// Append to results
this.results.$element.append( this.rows[row].$element );
}
return row;
};
/**
* Respond to add results event in the results widget.
* Override the way SelectWidget and GroupElement append the items
* into the group so we can append them in groups of rows.
* @param {ve.ui.MWMediaResultWidget[]} items An array of item elements
*/
ve.ui.MWMediaSearchWidget.prototype.onResultsAdd = function ( items ) {
var search = this;
// Add method to a queue; this queue will only run when the widget
// is visible
this.layoutQueue.push( function () {
var i, j, ilen, jlen, itemWidth, row, effectiveWidth,
resizeFactor,
maxRowWidth = search.results.$element.width() - 15;
// Go over the added items
row = search.getAvailableRow();
for ( i = 0, ilen = items.length; i < ilen; i++ ) {
itemWidth = items[i].$element.outerWidth( true );
// Add items to row until it is full
if ( search.rows[row].width + itemWidth >= maxRowWidth ) {
// Mark this row as full
search.rows[row].isFull = true;
search.rows[row].$element.attr( 'data-full', true );
// Find the resize factor
effectiveWidth = search.rows[row].width;
resizeFactor = maxRowWidth / effectiveWidth;
search.rows[row].$element.attr( 'data-effectiveWidth', effectiveWidth );
search.rows[row].$element.attr( 'data-resizeFactor', resizeFactor );
search.rows[row].$element.attr( 'data-row', row );
// Resize all images in the row to fit the width
for ( j = 0, jlen = search.rows[row].items.length; j < jlen; j++ ) {
search.rows[row].items[j].resizeThumb( resizeFactor );
}
// find another row
row = search.getAvailableRow();
}
// Add the commulative
search.rows[row].width += itemWidth;
// Store reference to the item and to the row
search.rows[row].items.push( items[i] );
items[i].setRow( row );
// Append the item
search.rows[row].$element.append( items[i].$element );
}
// If we have less than 4 rows, call for more images
if ( search.rows.length < 4 ) {
search.queryMediaQueue();
}
} );
this.runLayoutQueue();
};
/**
* Run layout methods from the queue only if the element is visible.
*/
ve.ui.MWMediaSearchWidget.prototype.runLayoutQueue = function () {
var i, len;
if ( this.$element.is( ':visible' ) ) {
for ( i = 0, len = this.layoutQueue.length; i < len; i++ ) {
this.layoutQueue.pop()();
}
}
};
/**
* Respond to removing results event in the results widget.
* Clear the relevant rows.
* @param {OO.ui.OptionWidget[]} items Removed items
*/
ve.ui.MWMediaSearchWidget.prototype.onResultsRemove = function ( items ) {
if ( items.length > 0 ) {
// In the case of the media search widget, if any items are removed
// all are removed (new search)
this.resetRows();
}
};
/**
* Set language for the search results.
* @param {string} lang Language
*/
ve.ui.MWMediaSearchWidget.prototype.setLang = function ( lang ) {
this.lang = lang;
};
/**
* Get language for the search results.
* @returns {string} lang Language
*/
ve.ui.MWMediaSearchWidget.prototype.getLang = function () {
return this.lang;
};