Merge "Refactor MWMediaSearchWidget to use a queue and providers"

This commit is contained in:
jenkins-bot 2015-02-07 01:15:38 +00:00 committed by Gerrit Code Review
commit ac9f81cd2c
9 changed files with 818 additions and 274 deletions

View file

@ -14,6 +14,12 @@
"classes": [
"ve.ui.MW*Page"
]
},
{
"name": "Data Model",
"classes": [
"ve.dm.MWMediaResource*"
]
}
]
},

View file

@ -14,6 +14,12 @@
"classes": [
"ve.ui.MW*Page"
]
},
{
"name": "Data Model",
"classes": [
"ve.dm.MWMediaResource*"
]
}
]
},

View file

@ -982,6 +982,8 @@ $wgResourceModules += array(
'ext.visualEditor.mwimage' => $wgVisualEditorResourceTemplate + array(
'scripts' => array(
'modules/ve-mw/dm/models/ve.dm.MWImageModel.js',
'modules/ve-mw/dm/models/ve.dm.MWMediaResourceProvider.js',
'modules/ve-mw/dm/models/ve.dm.MWMediaResourceQueue.js',
'modules/ve-mw/ui/widgets/ve.ui.MWMediaSearchWidget.js',
'modules/ve-mw/ui/widgets/ve.ui.MWMediaResultWidget.js',

View file

@ -963,6 +963,8 @@
"remoteExtPath": "VisualEditor",
"scripts": [
"modules/ve-mw/dm/models/ve.dm.MWImageModel.js",
"modules/ve-mw/dm/models/ve.dm.MWMediaResourceProvider.js",
"modules/ve-mw/dm/models/ve.dm.MWMediaResourceQueue.js",
"modules/ve-mw/ui/widgets/ve.ui.MWMediaSearchWidget.js",
"modules/ve-mw/ui/widgets/ve.ui.MWMediaResultWidget.js",
"modules/ve-mw/ui/widgets/ve.ui.MWMediaInfoFieldWidget.js",

View file

@ -0,0 +1,509 @@
/*!
* VisualEditor DataModel MWMediaResourceProvider class.
*
* @copyright 2011-2015 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* MediaWiki media resource provider.
*
* @class
* @mixins OO.EventEmitter
*
* @constructor
* @param {Object} [config] Configuration options
*/
ve.dm.MWMediaResourceProvider = function VeDmMWMediaResourceProvider( config ) {
config = config || {};
// Source Configuration
this.apiurl = this.setAPIurl( config.apiurl );
this.name = config.name;
this.displayName = config.displayName;
this.local = config.local;
this.scriptDirUrl = config.scriptDirUrl;
// ajaxOptions configuration
this.dataType = config.dataType || 'jsonp';
this.cached = config.cached || true;
// Fetching configuration
this.fetchLimit = config.limit || 30;
this.iiprop = config.iiprop || [ 'dimensions', 'url', 'mediatype', 'extmetadata', 'timestamp' ];
this.fetchProp = config.fetchProp || 'imageinfo';
this.lang = config.lang || 'en';
this.siteInfoPromise = null;
this.thumbSizes = [];
this.imageSizes = [];
this.depleted = false;
this.offset = config.offset || 0;
this.setQuery( config.query || '' );
// Mixin constructors
OO.EventEmitter.call( this );
};
/* Setup */
OO.initClass( ve.dm.MWMediaResourceProvider );
OO.mixinClass( ve.dm.MWMediaResourceProvider, OO.EventEmitter );
/* Methods */
/**
* Initialize the source and get the site info.
*
* Connect to the api url and retrieve the siteinfo parameters
* that are required for fetching results.
*
* @return {jQuery.Promise} Promise that resolves when the class
* properties are set.
*/
ve.dm.MWMediaResourceProvider.prototype.loadSiteInfo = function () {
var provider = this;
if ( !this.siteInfoPromise ) {
this.siteInfoPromise = ve.init.target.constructor.static.apiRequest( {
action: 'query',
meta: 'siteinfo'
} )
.then( function ( data ) {
if ( data.error ) {
return $.Deferred().reject();
}
provider.setImageSizes( ve.getProp( data, 'query', 'general', 'imagelimits' ) || [] );
provider.setThumbSizes( ve.getProp( data, 'query', 'general', 'thumblimits' ) || [] );
} );
}
return this.siteInfoPromise;
};
/**
* Get results from the source
*
* @return {jQuery.Promise} Promise that is resolved into an array
* of available results, or is rejected if no results are available.
*/
ve.dm.MWMediaResourceProvider.prototype.getResults = function ( howMany ) {
var xhr,
aborted = false,
provider = this;
return this.loadSiteInfo()
.then( function () {
if ( aborted ) {
return $.Deferred().reject();
}
xhr = provider.fetchAPIresults( howMany );
return xhr;
} )
.then(
function ( results ) {
if ( results.length === 0 ) {
provider.toggleDepleted( true );
}
return results;
},
// Process failed, return an empty promise
function () {
provider.toggleDepleted( true );
return $.Deferred().resolve( [] );
}
)
.promise( { abort: function () {
aborted = true;
if ( xhr ) {
xhr.abort();
}
} } );
};
/**
* Call the API for search results.
*
* @param {number} howMany The number of results to retrieve
* @return {jQuery.Promise} Promise that resolves with an array of objects that contain
* the fetched data.
*/
ve.dm.MWMediaResourceProvider.prototype.fetchAPIresults = function ( howMany ) {
var xhr,
ajaxOptions = {},
query = this.getQuery(),
provider = this,
apiCallConfig = {
action: 'query',
generator: 'search',
gsrsearch: query,
gsrnamespace: 6,
continue: '',
gsroffset: this.getOffset(),
prop: this.getFetchProp(),
// Language of the extmetadata details
iiextmetadatalanguage: this.getLang(),
iiprop: this.getIiProp().join( '|' ),
iiurlheight: this.getMaxHeight(),
// Standard width per resource
iiurlwidth: this.getStandardWidth()
};
howMany = howMany || 20;
// Initial number of images
apiCallConfig.gsrlimit = howMany;
if ( this.isValid() ) {
if ( this.isLocal() ) {
ajaxOptions = {
url: mw.util.wikiScript( 'api' ),
// If the url is local use json
dataType: 'json'
};
} else {
ajaxOptions = {
// If 'apiurl' is set, use that. Otherwise, build the url
// from scriptDirUrl and /api.php suffix
url: this.apiurl || ( this.scriptDirUrl + '/api.php' ),
// If the url is not the same origin use jsonp
dataType: 'jsonp',
// JSON-P requests are not cached by default and get a &_=random trail.
// While setting cache=true will still bypass cache in most case due to the
// callback parameter, at least drop the &_=random trail which triggers
// an API warning (invalid parameter).
cache: true
};
}
xhr = ve.init.target.constructor.static.apiRequest( apiCallConfig, ajaxOptions );
return xhr
.then( function ( data ) {
var page, newObj,
results = [],
raw = ve.getProp( data, 'query', 'pages' );
if ( data[ 'continue' ] ) {
// Update the offset for next time
provider.setOffset( data[ 'continue' ].gsroffset );
} else {
// This is the last available set of result. Mark as depleted!
provider.toggleDepleted( true );
}
if ( raw ) {
// Strip away the page ids
for ( page in raw ) {
newObj = raw[page].imageinfo[0];
newObj.title = raw[page].title;
results.push( newObj );
}
}
return results;
} )
.promise( { abort: xhr.abort } );
}
};
/**
* Get search query
*
* @return {string} search query
*/
ve.dm.MWMediaResourceProvider.prototype.getQuery = function () {
return this.query;
};
/**
* Set search query
*
* @param {string} value
*/
ve.dm.MWMediaResourceProvider.prototype.setQuery = function ( value ) {
if ( this.query !== value ) {
this.query = value;
// Reset offset
this.setOffset( 0 );
// Reset depleted status
this.toggleDepleted( false );
}
};
/**
* Set api url
*
* @param {string} API url
*/
ve.dm.MWMediaResourceProvider.prototype.setAPIurl = function ( url ) {
this.apiurl = url;
};
/**
* Set api url
*
* @return {string} API url
*/
ve.dm.MWMediaResourceProvider.prototype.getAPIurl = function () {
return this.apiurl;
};
/**
* Set name
*
* @param {string} name
*/
ve.dm.MWMediaResourceProvider.prototype.setName = function ( name ) {
this.name = name;
};
/**
* Get name
*
* @returns {string} name
*/
ve.dm.MWMediaResourceProvider.prototype.getName = function () {
return this.name;
};
/**
* Get displayName
*
* @return {string} displayName
*/
ve.dm.MWMediaResourceProvider.prototype.getDisplayName = function () {
return this.displayName;
};
/**
* Set displayName
*
* @param {string} displayName
*/
ve.dm.MWMediaResourceProvider.prototype.setDisplayName = function ( displayName ) {
this.displayName = displayName;
};
/**
* Get isLocal value
*
* @return {boolean} isLocal value
*/
ve.dm.MWMediaResourceProvider.prototype.isLocal = function () {
return this.local;
};
/**
* Get ScriptDirUrl
*
* @return {string} ScriptDirUrl
*/
ve.dm.MWMediaResourceProvider.prototype.getScriptDirUrl = function () {
return this.scriptDirUrl;
};
/**
* Set scriptDirUrl
*
* @param {string} scriptDirUrl
*/
ve.dm.MWMediaResourceProvider.prototype.setScriptDirUrl = function ( scriptDirUrl ) {
this.scriptDirUrl = scriptDirUrl;
};
/**
* Get dataType
*
* @return {string} dataType
*/
ve.dm.MWMediaResourceProvider.prototype.getDataType = function () {
return this.dataType;
};
/**
* Set dataType
*
* @param {string} dataType
*/
ve.dm.MWMediaResourceProvider.prototype.setDataType = function ( dataType ) {
this.dataType = dataType;
};
/**
* Get cached
*
* @return {boolean} cached
*/
ve.dm.MWMediaResourceProvider.prototype.isCached = function () {
return this.cached;
};
/**
* Get fetch limit or 'page' size. This is the number
* of results per request.
*
* @return {number} limit
*/
ve.dm.MWMediaResourceProvider.prototype.getFetchLimit = function () {
return this.limit;
};
/**
* Set limit
*
* @param {number} limit
*/
ve.dm.MWMediaResourceProvider.prototype.setFetchLimit = function ( limit ) {
this.limit = limit;
};
/**
* Get properties
*
* @return {string[]} properties
*/
ve.dm.MWMediaResourceProvider.prototype.getIiProp = function () {
return this.iiprop;
};
/**
* Get max height
*
* @return {number|undefined} Maximum height
*/
ve.dm.MWMediaResourceProvider.prototype.getMaxHeight = function () {
return this.maxHeight;
};
/**
* Set maximum height
*
* @param {number} Maximum height
*/
ve.dm.MWMediaResourceProvider.prototype.setMaxHeight = function ( maxHeight ) {
this.maxHeight = maxHeight;
};
/**
* Get standard width, based on the provider source's thumb sizes.
*
* @return {number|undefined} fetchWidth
*/
ve.dm.MWMediaResourceProvider.prototype.getStandardWidth = function () {
return this.thumbSizes && this.thumbSizes[ this.thumbSizes.length - 1 ];
};
/**
* Get prop
*
* @return {string} prop
*/
ve.dm.MWMediaResourceProvider.prototype.getFetchProp = function () {
return this.fetchProp;
};
/**
* Set prop
*
* @param {string} prop
*/
ve.dm.MWMediaResourceProvider.prototype.setFetchProp = function ( prop ) {
this.fetchProp = prop;
};
/**
* Get lang
*
* @return {string} lang
*/
ve.dm.MWMediaResourceProvider.prototype.getLang = function () {
return this.lang;
};
/**
* Set lang
*
* @param {string} lang
*/
ve.dm.MWMediaResourceProvider.prototype.setLang = function ( lang ) {
this.lang = lang;
};
/**
* Get Offset
*
* @return {number} Offset
*/
ve.dm.MWMediaResourceProvider.prototype.getOffset = function () {
return this.offset;
};
/**
* Set Offset
*
* @param {number} Offset
*/
ve.dm.MWMediaResourceProvider.prototype.setOffset = function ( offset ) {
this.offset = offset;
};
/**
* Set thumb sizes
*
* @param {number[]} sizes Available thumbnail sizes
*/
ve.dm.MWMediaResourceProvider.prototype.setThumbSizes = function ( sizes ) {
this.thumbSizes = sizes;
};
/**
* Set image sizes
*
* @param {number[]} sizes Available image sizes
*/
ve.dm.MWMediaResourceProvider.prototype.setImageSizes = function ( sizes ) {
this.imageSizes = sizes;
};
/**
* Get thumb sizes
*
* @returns {number[]} sizes Available thumbnail sizes
*/
ve.dm.MWMediaResourceProvider.prototype.getThumbSizes = function () {
return this.thumbSizes;
};
/**
* Get image sizes
*
* @returns {number[]} sizes Available image sizes
*/
ve.dm.MWMediaResourceProvider.prototype.getImageSizes = function () {
return this.imageSizes;
};
/**
* Check whether the provider is depleted
*
* @return {boolean} depleted
*/
ve.dm.MWMediaResourceProvider.prototype.isDepleted = function () {
return this.depleted;
};
/**
* Toggle depleted state
*
* @param {boolean} depleted
*/
ve.dm.MWMediaResourceProvider.prototype.toggleDepleted = function ( isDepleted ) {
this.depleted = isDepleted !== undefined ? isDepleted : !this.depleted;
};
/**
* Check if this source is valid and ready for search.
* @return {boolean} Source is valid
*/
ve.dm.MWMediaResourceProvider.prototype.isValid = function () {
return this.getQuery() &&
(
// If we don't have either 'apiurl' or 'scriptDirUrl'
// the source is invalid, and we will skip it
this.apiurl || this.scriptDirUrl !== undefined
);
};

View file

@ -0,0 +1,174 @@
/*!
* VisualEditor DataModel MWMediaResourceQueue class.
*
* @copyright 2011-2015 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* MediaWiki media resource queue.
*
* @class
* @mixins OO.EventEmitter
*
* @constructor
* @param {Object} [config] Configuration options
*/
ve.dm.MWMediaResourceQueue = function VeDmMWMediaResourceQueue( config ) {
config = config || {};
this.fileRepoPromise = null;
this.providers = [];
this.providerPromises = [];
this.queue = [];
this.limit = config.limit || 20;
this.threshhold = config.threshhold || 10;
// Mixin constructors
OO.EventEmitter.call( this );
};
/* Setup */
OO.initClass( ve.dm.MWMediaResourceQueue );
OO.mixinClass( ve.dm.MWMediaResourceQueue, OO.EventEmitter );
/**
* Get items from the queue
*
* @param {number} [howMany] How many items to retrieve
* @return {jQuery.Promise} Promise that resolves into an array of items
*/
ve.dm.MWMediaResourceQueue.prototype.get = function ( howMany ) {
var me = this,
prepared = [];
howMany = howMany || this.limit;
// Check if the queue has enough items
if ( this.queue.length < howMany + this.threshhold ) {
// Call for more results
prepared.push(
this.getResults( howMany + this.threshhold )
.then( function ( items ) {
// Add to the queue
me.queue = me.queue.concat.apply( me.queue, items );
} )
);
}
return $.when.apply( $, prepared )
.then( function () {
return me.queue.splice( 0, howMany );
} );
};
/**
* Get results from all providers
* @return {jQuery.Promise} Promise that is resolved into an array of fetched items.
*/
ve.dm.MWMediaResourceQueue.prototype.getResults = function ( howMany ) {
var i, len,
queue = this;
// Make sure there are resources set up
return this.setup()
.then( function () {
queue.providerPromises = [];
// Set up the query to all providers
for ( i = 0, len = queue.providers.length; i < len; i++ ) {
queue.providers[i].setQuery( queue.getQuery() );
if ( !queue.providers[i].isDepleted() ) {
queue.providerPromises.push(
queue.providers[i].getResults( howMany )
);
}
}
return $.when.apply( $, queue.providerPromises )
.then( Array.prototype.concat.bind( [] ) );
} );
};
/**
* Set up the queue and its resources
*
* @return {jQuery.Promise} Promise that resolves when the resources are set up
*/
ve.dm.MWMediaResourceQueue.prototype.setup = function () {
var i, len,
queue = this;
return this.getFileRepos().then( function ( sources ) {
if ( queue.providers.length === 0 ) {
// Set up the providers
for ( i = 0, len = sources.length; i < len; i++ ) {
queue.providers.push( new ve.dm.MWMediaResourceProvider( {
apiurl: sources[i].apiurl,
name: sources[i].name,
local: sources[i].local,
scriptDirUrl: sources[i].scriptDirUrl
} ) );
}
}
} );
};
/**
* Fetch the file repos.
*
* @return {jQuery.Promise} Promise that resolves when the resources are set up
*/
ve.dm.MWMediaResourceQueue.prototype.getFileRepos = function () {
var defaultSource = [ {
url: mw.util.wikiScript( 'api' ),
local: true
} ];
if ( !this.fileRepoPromise ) {
this.fileRepoPromise = ve.init.target.constructor.static.apiRequest( {
action: 'query',
meta: 'filerepoinfo'
} ).then(
function ( resp ) {
return resp.query && resp.query.repos || defaultSource;
},
function () {
return $.Deferred().resolve( defaultSource );
}
);
}
return this.fileRepoPromise;
};
/**
* Set the search query for all the providers.
*
* This also makes sure to abort any previous promises.
*
* @param {string} query Search query
*/
ve.dm.MWMediaResourceQueue.prototype.setQuery = function ( query ) {
var i, len;
if ( query !== this.query ) {
this.query = query;
// Reset queue
this.queue = [];
// Reset promises
for ( i = 0, len = this.providerPromises.length; i < len; i++ ) {
this.providerPromises[i].abort();
}
}
};
/**
* Get the current search query.
*
* @param {string} query Search query
*/
ve.dm.MWMediaResourceQueue.prototype.getQuery = function () {
return this.query;
};

View file

@ -410,15 +410,14 @@ ve.ui.MWMediaDialog.prototype.initialize = function () {
* Note: Some information in the metadata object needs to be safely
* stripped from its html wrappers.
*
* @param {Object} info Image info
* @param {Object} imageinfo Image info
*/
ve.ui.MWMediaDialog.prototype.buildMediaInfoPanel = function ( info ) {
ve.ui.MWMediaDialog.prototype.buildMediaInfoPanel = function ( imageinfo ) {
var i, newDimensions, field, isPortrait, $info, $section, windowWidth,
contentDirection = this.getFragment().getDocument().getDir(),
imageinfo = info.imageinfo[0],
imageTitle = new OO.ui.LabelWidget( {
$: this.$,
label: new mw.Title( info.title ).getNameText()
label: new mw.Title( imageinfo.title ).getNameText()
} ),
metadata = imageinfo.extmetadata,
// Field configuration (in order)
@ -620,7 +619,7 @@ ve.ui.MWMediaDialog.prototype.buildMediaInfoPanel = function ( info ) {
} );
// Call for a bigger image
this.fetchThumbnail( info.title, newDimensions )
this.fetchThumbnail( imageinfo.title, newDimensions )
.done( function ( thumburl ) {
if ( thumburl ) {
$image.prop( 'src', thumburl );
@ -779,21 +778,18 @@ ve.ui.MWMediaDialog.prototype.onSearchChoose = function ( info ) {
* @param {ve.ui.MWMediaResultWidget|null} item Selected item
*/
ve.ui.MWMediaDialog.prototype.confirmSelectedImage = function () {
var info,
obj = {},
item = this.selectedImageInfo;
if ( item ) {
info = item.imageinfo[0];
var obj = {},
info = this.selectedImageInfo;
if ( info ) {
if ( !this.imageModel ) {
// Create a new image model based on default attributes
this.imageModel = ve.dm.MWImageModel.static.newFromImageAttributes(
{
// Per https://www.mediawiki.org/w/?diff=931265&oldid=prev
href: './' + item.title,
href: './' + info.title,
src: info.url,
resource: './' + item.title,
resource: './' + info.title,
width: info.thumbwidth,
height: info.thumbheight,
mediaType: info.mediatype,
@ -811,9 +807,9 @@ ve.ui.MWMediaDialog.prototype.confirmSelectedImage = function () {
this.imageModel.changeImageSource(
{
mediaType: info.mediatype,
href: './' + item.title,
href: './' + info.title,
src: info.url,
resource: './' + item.title
resource: './' + info.title
},
info
);
@ -824,7 +820,7 @@ ve.ui.MWMediaDialog.prototype.confirmSelectedImage = function () {
}
// Cache
obj[ item.title ] = info;
obj[ info.title ] = info;
ve.init.platform.imageInfoCache.set( obj );
this.checkChanged();
@ -988,34 +984,6 @@ ve.ui.MWMediaDialog.prototype.checkChanged = function () {
}
};
/**
* Get the object of file repos to use for the media search
*
* @returns {jQuery.Promise}
*/
ve.ui.MWMediaDialog.prototype.getFileRepos = function () {
var defaultSource = [ {
url: mw.util.wikiScript( 'api' ),
local: true
} ];
if ( !this.fileRepoPromise ) {
this.fileRepoPromise = ve.init.target.constructor.static.apiRequest( {
action: 'query',
meta: 'filerepoinfo'
} ).then(
function ( resp ) {
return resp.query && resp.query.repos || defaultSource;
},
function () {
return $.Deferred().resolve( defaultSource );
}
);
}
return this.fileRepoPromise;
};
/**
* @inheritdoc
*/
@ -1093,31 +1061,8 @@ ve.ui.MWMediaDialog.prototype.switchPanels = function ( panel, stopSearchRequery
this.setSize( 'larger' );
this.selectedImageInfo = null;
if ( !stopSearchRequery ) {
// Show a spinner while we check for file repos.
// this will only be done once per session.
// The filerepo promise will be sent to the API
// only once per session so this will be resolved
// every time the search panel reloads
this.$spinner.removeClass( 'oo-ui-element-hidden' );
this.search.toggle( false );
// Get the repos from the API first
// The ajax request will only be done once per session
dialog.getFileRepos().done( function ( repos ) {
dialog.search.setSources( repos );
// Done, hide the spinner
dialog.$spinner.addClass( 'oo-ui-element-hidden' );
// Show the search and query the media sources
dialog.search.toggle( true );
dialog.search.query.setValue( dialog.pageTitle );
dialog.search.queryMediaSources();
// Initialization
// This must be done only after there are proper
// sources defined
dialog.search.getQuery().focus().select();
dialog.search.getResults().selectItem();
dialog.search.getResults().highlightItem();
} );
this.search.query.setValue( dialog.pageTitle );
this.search.query.focus().select();
}
// Set the edit panel

View file

@ -24,14 +24,15 @@ ve.ui.MWMediaResultWidget = function VeUiMWMediaResultWidget( config ) {
// Properties
this.initialSize = config.size || 150;
this.maxSize = config.maxSize || this.initialSize * 2;
this.maxWidth = config.maxWidth || this.initialSize * 2;
this.expanded = false;
this.dimensions = {};
this.$thumb = this.buildThumbnail();
this.$overlay = this.$( '<div>' );
this.row = null;
// Store the thumbnail url
this.thumbUrl = ve.getProp( this.data.imageinfo, 0, 'thumburl' );
this.thumbUrl = this.data.thumburl;
this.src = null;
// Get wiki default thumbnail size
this.defaultThumbSize = mw.config.get( 'wgVisualEditorConfig' )
@ -48,15 +49,6 @@ ve.ui.MWMediaResultWidget = function VeUiMWMediaResultWidget( config ) {
// Adjust wrapper padding
this.$element.css( $.extend( this.dimensions, this.calculateWrapperPadding( this.dimensions, this.initialSize ) ) );
// Select button
this.selectButton = new OO.ui.ButtonWidget( {
$: this.$,
label: ve.msg( 'visualeditor-dialog-media-searchselect' ),
icon: 'check'
} );
this.selectButton.toggle( false );
this.$element.prepend( this.selectButton.$element );
};
/* Inheritance */
@ -90,7 +82,7 @@ ve.ui.MWMediaResultWidget.prototype.onThumbnailError = function () {
*/
ve.ui.MWMediaResultWidget.prototype.buildThumbnail = function () {
var imageDimensions,
info = this.data.imageinfo[0],
info = this.data,
$thumb = this.$( '<img>' );
// Preload image
@ -109,7 +101,7 @@ ve.ui.MWMediaResultWidget.prototype.buildThumbnail = function () {
height: this.initialSize
};
} else {
if ( info.height < this.initialSize && info.width < this.maxSize ) {
if ( info.height < this.initialSize && info.width < this.maxWidth ) {
// Define dimensions with original size
imageDimensions = {
width: info.width,
@ -137,7 +129,10 @@ ve.ui.MWMediaResultWidget.prototype.buildThumbnail = function () {
* actual src.
*/
ve.ui.MWMediaResultWidget.prototype.lazyLoad = function () {
this.$thumb.attr( 'src', this.thumbUrl );
if ( !this.hasSrc() ) {
this.src = this.thumbUrl;
this.$thumb.attr( 'src', this.thumbUrl );
}
};
/**
@ -176,7 +171,7 @@ ve.ui.MWMediaResultWidget.prototype.resizeThumb = function ( resizeFactor ) {
*/
ve.ui.MWMediaResultWidget.prototype.calculateThumbDimensions = function ( imageDimensions ) {
var dimensions,
maxWidth = this.maxSize,
maxWidth = this.maxWidth,
ratio = imageDimensions.width / imageDimensions.height;
// Rules of resizing:
// (1) Images must have height = this.initialSize
@ -263,5 +258,5 @@ ve.ui.MWMediaResultWidget.prototype.getRow = function () {
* @returns {boolean} Thumbnail has its source attribute set
*/
ve.ui.MWMediaResultWidget.prototype.hasSrc = function () {
return !!this.$thumb.attr( 'src' );
return !!this.src;
};

View file

@ -25,29 +25,38 @@ ve.ui.MWMediaSearchWidget = function VeUiMWMediaSearchWidget( config ) {
OO.ui.SearchWidget.call( this, config );
// Properties
this.sources = {};
this.providers = {};
this.searchValue = '';
this.rowHeight = config.rowHeight || 200;
this.$panels = config.$panels;
this.resourceQueue = new ve.dm.MWMediaResourceQueue( {
limit: 20,
threshhold: 10
} );
this.queryTimeout = null;
this.titles = {};
this.queryMediaSourcesCallback = this.queryMediaSources.bind( this );
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.lang = config.lang || 'en';
this.selected = null;
this.rows = [];
this.$noItemsMessage = this.$( '<div>' )
.addClass( 've-ui-mwMediaSearchWidget-noresults oo-ui-element-hidden' )
.text( ve.msg( 'visualeditor-dialog-media-noresults' ) )
.appendTo( this.$query );
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',
@ -65,29 +74,78 @@ OO.inheritClass( ve.ui.MWMediaSearchWidget, OO.ui.SearchWidget );
/* Methods */
/**
* Set the fileRepo sources for the media search
* @param {Object} sources The sources object
* Query all sources for media.
*
* @method
*/
ve.ui.MWMediaSearchWidget.prototype.setSources = function ( sources ) {
this.sources = sources;
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();
}
} );
};
/**
* Set the fileRepo sources for the media search
* @param {Object} sources The sources object
* Process the media queue giving more items
*
* @method
* @param {Object[]} items Given items by the media queue
*/
ve.ui.MWMediaSearchWidget.prototype.getSources = function () {
return this.sources;
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 select widget select events.
* Handle search value change
*
* @param {string} value New value
*/
ve.ui.MWMediaSearchWidget.prototype.onQueryChange = function ( value ) {
var i, len,
trimmed = $.trim( value );
var trimmed = $.trim( value );
if ( trimmed === this.searchValue ) {
return;
@ -98,14 +156,18 @@ ve.ui.MWMediaSearchWidget.prototype.onQueryChange = function ( value ) {
OO.ui.SearchWidget.prototype.onQueryChange.apply( this, arguments );
// Reset
this.titles = {};
for ( i = 0, len = this.sources.length; i < len; i++ ) {
delete this.sources[i].gsroffset;
}
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.queryMediaSourcesCallback, 250 );
this.queryTimeout = setTimeout( this.queryMediaQueueCallback, 350 );
};
/**
@ -128,7 +190,7 @@ ve.ui.MWMediaSearchWidget.prototype.onResultsScroll = function () {
// Check if we need to ask for more results
if ( !this.query.isPending() && position > threshold ) {
this.queryMediaSources();
this.queryMediaQueue();
}
this.lazyLoadResults();
@ -142,6 +204,7 @@ ve.ui.MWMediaSearchWidget.prototype.lazyLoadResults = function () {
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;
@ -151,82 +214,6 @@ ve.ui.MWMediaSearchWidget.prototype.lazyLoadResults = function () {
}
}
};
/**
* Query all sources for media.
*
* @method
*/
ve.ui.MWMediaSearchWidget.prototype.queryMediaSources = function () {
var i, len, source, request,
lang = this.getLang(),
ajaxOptions = {},
value = this.query.getValue();
if ( value === '' ) {
return;
}
// Reset message
this.$noItemsMessage.addClass( 'oo-ui-element-hidden' );
// Abort previous promises if they are pending
this.resetPromises();
for ( i = 0, len = this.sources.length; i < len; i++ ) {
source = this.sources[i];
// If we don't have either 'apiurl' or 'scriptDirUrl'
// the source is invalid, and we will skip it
if ( source.apiurl || source.scriptDirUrl !== undefined ) {
if ( !source.gsroffset ) {
source.gsroffset = 0;
}
if ( source.local ) {
ajaxOptions = {
url: mw.util.wikiScript( 'api' ),
// If the url is local use json
dataType: 'json'
};
} else {
ajaxOptions = {
// If 'apiurl' is set, use that. Otherwise, build the url
// from scriptDirUrl and /api.php suffix
url: source.apiurl || ( source.scriptDirUrl + '/api.php' ),
// If the url is not the same origin use jsonp
dataType: 'jsonp',
// JSON-P requests are not cached by default and get a &_=random trail.
// While setting cache=true will still bypass cache in most case due to the
// callback parameter, at least drop the &_=random trail which triggers
// an API warning (invalid parameter).
cache: true
};
}
this.query.pushPending();
request = ve.init.target.constructor.static.apiRequest( {
action: 'query',
generator: 'search',
gsrsearch: value,
gsrnamespace: 6,
// Initial number of images
// NOTE: If this is too high, it triggers Common's bot prevention code
gsrlimit: 30,
gsroffset: source.gsroffset,
prop: 'imageinfo',
// Language of the extmetadata details
iiextmetadatalanguage: lang,
iiprop: 'dimensions|url|mediatype|extmetadata|timestamp',
iiurlheight: this.rowHeight,
// Width of the dialog
iiurlwidth: 600 - 30 // Take off 30px for the margins
}, ajaxOptions )
.done( this.onMediaQueryDone.bind( this, source ) );
source.value = value;
this.promises.push( request );
}
// When all sources are done, check to see if there are results
$.when.apply( $, this.promises ).done( this.onAllMediaQueriesDone.bind( this ) );
}
};
/**
* Reset all the rows; destroy the jQuery elements and reset
@ -242,44 +229,10 @@ ve.ui.MWMediaSearchWidget.prototype.resetRows = function () {
this.rows = [];
};
/**
* Abort all api search query promises
*/
ve.ui.MWMediaSearchWidget.prototype.resetPromises = function () {
var i;
for ( i = 0; i < this.promises.length; i++ ) {
this.promises[i].abort();
this.query.popPending();
}
this.rowIndex = 0;
// Empty the promise array
this.promises = [];
// Empty the results queue
this.layoutQueue = [];
};
/**
* Handle media query response events.
*
* @method
* @param {Object} source Media query source
*/
ve.ui.MWMediaSearchWidget.prototype.onAllMediaQueriesDone = function () {
this.query.popPending();
if ( this.results.getItems().length === 0 ) {
this.$noItemsMessage.removeClass( 'oo-ui-element-hidden' );
} else {
this.$noItemsMessage.addClass( 'oo-ui-element-hidden' );
this.lazyLoadResults();
}
};
/**
* 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 () {
@ -377,7 +330,7 @@ ve.ui.MWMediaSearchWidget.prototype.onResultsAdd = function ( items ) {
// If we have less than 4 rows, call for more images
if ( search.rows.length < 4 ) {
search.queryMediaSources();
search.queryMediaQueue();
}
} );
this.runLayoutQueue();
@ -409,54 +362,6 @@ ve.ui.MWMediaSearchWidget.prototype.onResultsRemove = function ( items ) {
}
};
/**
* Handle media query load events.
*
* @method
* @param {Object} source Media query source
* @param {Object} data Media query response
*/
ve.ui.MWMediaSearchWidget.prototype.onMediaQueryDone = function ( source, data ) {
if ( !data.query || !data.query.pages ) {
return;
}
var page, title,
items = [],
pages = data.query.pages,
value = this.query.getValue();
if ( value === '' || value !== source.value ) {
return;
}
if ( data['query-continue'] && data['query-continue'].search ) {
source.gsroffset = data['query-continue'].search.gsroffset;
}
for ( page in pages ) {
// Verify that imageinfo exists
// In case it does not, skip the image to avoid errors in
// ve.ui.MWMediaResultWidget
if ( pages[page].imageinfo && pages[page].imageinfo.length > 0 ) {
title = new mw.Title( pages[page].title ).getMainText();
if ( !Object.prototype.hasOwnProperty.call( this.titles, title ) ) {
this.titles[title] = true;
items.push(
new ve.ui.MWMediaResultWidget( {
$: this.$,
data: pages[page],
size: this.rowHeight,
maxSize: this.results.$element.width() / 3
} )
);
}
}
}
this.results.addItems( items );
};
/**
* Set language for the search results.
* @param {string} lang Language