jenkins-bot has submitted this change and it was merged.

Change subject: Refactor MWMediaSearchWidget to use a queue and providers
......................................................................


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
---
M .docs/categories.json
M .docs/mw-categories.json
M VisualEditor.php
M extension.json
A modules/ve-mw/dm/models/ve.dm.MWMediaResourceProvider.js
A modules/ve-mw/dm/models/ve.dm.MWMediaResourceQueue.js
M modules/ve-mw/ui/dialogs/ve.ui.MWMediaDialog.js
M modules/ve-mw/ui/widgets/ve.ui.MWMediaResultWidget.js
M modules/ve-mw/ui/widgets/ve.ui.MWMediaSearchWidget.js
9 files changed, 818 insertions(+), 274 deletions(-)

Approvals:
  Trevor Parscal: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/.docs/categories.json b/.docs/categories.json
index aad7a11..7ef5618 100644
--- a/.docs/categories.json
+++ b/.docs/categories.json
@@ -14,6 +14,12 @@
                                "classes": [
                                        "ve.ui.MW*Page"
                                ]
+                       },
+                       {
+                               "name": "Data Model",
+                               "classes": [
+                                       "ve.dm.MWMediaResource*"
+                               ]
                        }
                ]
        },
diff --git a/.docs/mw-categories.json b/.docs/mw-categories.json
index 755c08d..19a0259 100644
--- a/.docs/mw-categories.json
+++ b/.docs/mw-categories.json
@@ -14,6 +14,12 @@
                                "classes": [
                                        "ve.ui.MW*Page"
                                ]
+                       },
+                       {
+                               "name": "Data Model",
+                               "classes": [
+                                       "ve.dm.MWMediaResource*"
+                               ]
                        }
                ]
        },
diff --git a/VisualEditor.php b/VisualEditor.php
index 5c6d7d4..0e83b56 100644
--- a/VisualEditor.php
+++ b/VisualEditor.php
@@ -982,6 +982,8 @@
        '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',
diff --git a/extension.json b/extension.json
index dd1d4f5..c3f1fe6 100644
--- a/extension.json
+++ b/extension.json
@@ -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",
diff --git a/modules/ve-mw/dm/models/ve.dm.MWMediaResourceProvider.js 
b/modules/ve-mw/dm/models/ve.dm.MWMediaResourceProvider.js
new file mode 100644
index 0000000..19a013b
--- /dev/null
+++ b/modules/ve-mw/dm/models/ve.dm.MWMediaResourceProvider.js
@@ -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
+               );
+};
diff --git a/modules/ve-mw/dm/models/ve.dm.MWMediaResourceQueue.js 
b/modules/ve-mw/dm/models/ve.dm.MWMediaResourceQueue.js
new file mode 100644
index 0000000..10ebc9a
--- /dev/null
+++ b/modules/ve-mw/dm/models/ve.dm.MWMediaResourceQueue.js
@@ -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;
+};
diff --git a/modules/ve-mw/ui/dialogs/ve.ui.MWMediaDialog.js 
b/modules/ve-mw/ui/dialogs/ve.ui.MWMediaDialog.js
index 697c138..21f9031 100644
--- a/modules/ve-mw/ui/dialogs/ve.ui.MWMediaDialog.js
+++ b/modules/ve-mw/ui/dialogs/ve.ui.MWMediaDialog.js
@@ -410,15 +410,14 @@
  * 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 @@
        } );
 
        // 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 @@
  * @param {ve.ui.MWMediaResultWidget|null} item Selected item
  */
 ve.ui.MWMediaDialog.prototype.confirmSelectedImage = function () {
-       var info,
-               obj = {},
-               item = this.selectedImageInfo;
+       var obj = {},
+               info = this.selectedImageInfo;
 
-       if ( item ) {
-               info = item.imageinfo[0];
-
+       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 @@
                        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 @@
                }
 
                // Cache
-               obj[ item.title ] = info;
+               obj[ info.title ] = info;
                ve.init.platform.imageInfoCache.set( obj );
 
                this.checkChanged();
@@ -989,34 +985,6 @@
 };
 
 /**
- * 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
  */
 ve.ui.MWMediaDialog.prototype.getSetupProcess = function ( data ) {
@@ -1093,31 +1061,8 @@
                        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
diff --git a/modules/ve-mw/ui/widgets/ve.ui.MWMediaResultWidget.js 
b/modules/ve-mw/ui/widgets/ve.ui.MWMediaResultWidget.js
index 62d0421..253d814 100644
--- a/modules/ve-mw/ui/widgets/ve.ui.MWMediaResultWidget.js
+++ b/modules/ve-mw/ui/widgets/ve.ui.MWMediaResultWidget.js
@@ -24,14 +24,15 @@
 
        // 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 @@
 
        // 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.buildThumbnail = function () {
        var imageDimensions,
-               info = this.data.imageinfo[0],
+               info = this.data,
                $thumb = this.$( '<img>' );
 
        // Preload image
@@ -109,7 +101,7 @@
                        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 @@
  * 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.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 @@
  * @returns {boolean} Thumbnail has its source attribute set
  */
 ve.ui.MWMediaResultWidget.prototype.hasSrc = function () {
-       return !!this.$thumb.attr( 'src' );
+       return !!this.src;
 };
diff --git a/modules/ve-mw/ui/widgets/ve.ui.MWMediaSearchWidget.js 
b/modules/ve-mw/ui/widgets/ve.ui.MWMediaSearchWidget.js
index 94a5744..a2a6c4f 100644
--- a/modules/ve-mw/ui/widgets/ve.ui.MWMediaSearchWidget.js
+++ b/modules/ve-mw/ui/widgets/ve.ui.MWMediaSearchWidget.js
@@ -25,29 +25,38 @@
        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 @@
 /* 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 @@
        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 @@
 
        // 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 @@
                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;
@@ -149,82 +212,6 @@
                        // Load the image
                        items[i].lazyLoad();
                }
-       }
-};
-/**
- * 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 ) );
        }
 };
 
@@ -243,43 +230,9 @@
 };
 
 /**
- * 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 @@
 
                // If we have less than 4 rows, call for more images
                if ( search.rows.length < 4 ) {
-                       search.queryMediaSources();
+                       search.queryMediaQueue();
                }
        } );
        this.runLayoutQueue();
@@ -407,54 +360,6 @@
                // all are removed (new search)
                this.resetRows();
        }
-};
-
-/**
- * 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 );
 };
 
 /**

-- 
To view, visit https://gerrit.wikimedia.org/r/188725
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I65aed3446cd1f056476c56e6e04522c70e49e595
Gerrit-PatchSet: 17
Gerrit-Project: mediawiki/extensions/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Mooeypoo <[email protected]>
Gerrit-Reviewer: Catrope <[email protected]>
Gerrit-Reviewer: Legoktm <[email protected]>
Gerrit-Reviewer: Mooeypoo <[email protected]>
Gerrit-Reviewer: Trevor Parscal <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to