mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-25 14:56:20 +00:00
aa9eb95455
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
380 lines
9.4 KiB
JavaScript
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;
|
|
};
|