Alex Monk has uploaded a new change for review.
https://gerrit.wikimedia.org/r/189861
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
(cherry picked from commit aa9eb9545524242e9ff34de05d5b9b71485b81c4)
---
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(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/VisualEditor
refs/changes/61/189861/1
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/189861
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I65aed3446cd1f056476c56e6e04522c70e49e595
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/VisualEditor
Gerrit-Branch: wmf/1.25wmf16
Gerrit-Owner: Alex Monk <[email protected]>
Gerrit-Reviewer: Mooeypoo <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits