mediawiki-extensions-Visual.../modules/ve-mw/ui/widgets/ve.ui.MWMediaSearchWidget.js
Moriel Schottlender aa9eb95455 Refactor MWMediaSearchWidget to use a queue and providers
Change the media search widget to work with resource queues and
providers. Create providers based on the user's filerepo settings
and aggregate their responses with the media queue. Stop asking
for more results from providers that are depleted.

Also fixes a rather nasty infinite-loop bug where the API returns
only very few images, and the UI keeps asking for more.

Bug: T78161
Bug: T88764
Change-Id: I65aed3446cd1f056476c56e6e04522c70e49e595
2015-02-06 16:45:56 -08:00

380 lines
9.4 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,
threshhold: 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.setQuery( 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 = [],
value = this.resourceQueue.getQuery();
if ( value === '' || value !== this.query.getValue() ) {
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],
size: this.rowHeight,
maxWidth: this.results.$element.width() / 3
} )
);
}
}
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.setQuery( 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,
maxLineWidth = this.results.$element.innerWidth() - 10;
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( {
width: maxLineWidth,
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( {
width: maxLineWidth,
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,
maxLineWidth = search.results.$element.innerWidth() - 10;
// Go over the added items
row = search.getAvailableRow();
for ( i = 0, ilen = items.length; i < ilen; i++ ) {
// TODO: Figure out a better way to calculate the margins
// between images (for now, hard-coded as 6)
itemWidth = items[i].$element.outerWidth() + 6;
// Add items to row until it is full
if ( search.rows[row].width + itemWidth >= maxLineWidth ) {
// Mark this row as full
search.rows[row].isFull = true;
search.rows[row].$element.attr( 'data-full', true );
// Resize all images in the row to fit the width
effectiveWidth = search.rows[row].width;
resizeFactor = maxLineWidth / effectiveWidth;
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();
}
// Append to row
search.rows[row].width += itemWidth;
// Store reference to the item
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;
};