From 5aa030a9fbfb4b7004394f7e92ca6a417f491729 Mon Sep 17 00:00:00 2001 From: Ed Sanders Date: Thu, 17 Nov 2016 15:32:06 +0000 Subject: [PATCH] Bring in MediaSearchWidget from VE Bug: T140166 Change-Id: If53ef7f4b62c7a5d4da565c14dd2a353778694e5 --- languages/i18n/en.json | 2 + languages/i18n/qqq.json | 2 + resources/Resources.php | 26 + .../MediaSearch/broken-image.png | Bin 0 -> 1083 bytes .../mw.widgets.APIResultsProvider.js | 229 +++++++++ .../MediaSearch/mw.widgets.APIResultsQueue.js | 224 +++++++++ .../mw.widgets.MediaResourceProvider.js | 322 ++++++++++++ .../mw.widgets.MediaResourceQueue.js | 68 +++ .../mw.widgets.MediaResultWidget.css | 89 ++++ .../mw.widgets.MediaResultWidget.js | 274 +++++++++++ .../mw.widgets.MediaSearchProvider.js | 69 +++ .../mw.widgets.MediaSearchQueue.js | 82 ++++ .../mw.widgets.MediaSearchWidget.css | 10 + .../mw.widgets.MediaSearchWidget.js | 462 ++++++++++++++++++ 14 files changed, 1859 insertions(+) create mode 100644 resources/src/mediawiki.widgets/MediaSearch/broken-image.png create mode 100644 resources/src/mediawiki.widgets/MediaSearch/mw.widgets.APIResultsProvider.js create mode 100644 resources/src/mediawiki.widgets/MediaSearch/mw.widgets.APIResultsQueue.js create mode 100644 resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaResourceProvider.js create mode 100644 resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaResourceQueue.js create mode 100644 resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaResultWidget.css create mode 100644 resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaResultWidget.js create mode 100644 resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaSearchProvider.js create mode 100644 resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaSearchQueue.js create mode 100644 resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaSearchWidget.css create mode 100644 resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaSearchWidget.js diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 9aa0f4605f..3a8a7ae6b2 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -4102,6 +4102,8 @@ "mw-widgets-dateinput-no-date": "No date selected", "mw-widgets-dateinput-placeholder-day": "YYYY-MM-DD", "mw-widgets-dateinput-placeholder-month": "YYYY-MM", + "mw-widgets-mediasearch-input-placeholder": "Search for media", + "mw-widgets-mediasearch-noresults": "No results found.", "mw-widgets-titleinput-description-new-page": "page does not exist yet", "mw-widgets-titleinput-description-redirect": "redirect to $1", "mw-widgets-categoryselector-add-category-placeholder": "Add a category...", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 7327012152..d175c86fb1 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -4286,6 +4286,8 @@ "mw-widgets-dateinput-no-date": "Label of a date input field when no date has been selected.", "mw-widgets-dateinput-placeholder-day": "[[File:DateInputWidget active, empty.png|frame|Screenshot]]\nPlaceholder displayed in a date input field when it's empty, representing a date format with 4 digits for year, 2 digits for month, and 2 digits for day, separated with hyphens. This should be uppercase, if possible, and must not include any additional explanations. If there is no good way to translate it, make this message blank.", "mw-widgets-dateinput-placeholder-month": "Placeholder displayed in a date input field when it's empty, representing a date format with 4 digits for year and 2 digits for month, separated with hyphens (without a day). This should be uppercase, if possible, and must not include any additional explanations. If there is no good way to translate it, make this message blank.", + "mw-widgets-mediasearch-input-placeholder": "Place holder text for media search input", + "mw-widgets-mediasearch-noresults": "Label notifying the user no results were found for the media search.", "mw-widgets-titleinput-description-new-page": "Description label for a new page in the title input widget.", "mw-widgets-titleinput-description-redirect": "Description label for a redirect in the title input widget.", "mw-widgets-categoryselector-add-category-placeholder": "Placeholder displayed in the category selector widget after the capsules of already added categories.", diff --git a/resources/Resources.php b/resources/Resources.php index 587a84d89e..b37febdac8 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -2289,6 +2289,32 @@ return [ ], 'targets' => [ 'desktop', 'mobile' ], ], + 'mediawiki.widgets.MediaSearch' => [ + 'scripts' => [ + 'resources/src/mediawiki.widgets/MediaSearch/mw.widgets.APIResultsProvider.js', + 'resources/src/mediawiki.widgets/MediaSearch/mw.widgets.APIResultsQueue.js', + 'resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaResourceProvider.js', + 'resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaSearchProvider.js', + 'resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaResourceQueue.js', + 'resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaSearchQueue.js', + 'resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaSearchWidget.js', + 'resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaResultWidget.js', + ], + 'styles' => [ + 'resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaSearchWidget.css', + 'resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaResultWidget.css', + ], + 'dependencies' => [ + 'oojs-ui-widgets', + 'mediawiki.ForeignApi', + 'mediawiki.Title', + ], + 'messages' => [ + 'mw-widgets-mediasearch-noresults', + 'mw-widgets-mediasearch-input-placeholder', + ], + 'targets' => [ 'desktop', 'mobile' ], + ], 'mediawiki.widgets.UserInputWidget' => [ 'scripts' => [ 'resources/src/mediawiki.widgets/mw.widgets.UserInputWidget.js', diff --git a/resources/src/mediawiki.widgets/MediaSearch/broken-image.png b/resources/src/mediawiki.widgets/MediaSearch/broken-image.png new file mode 100644 index 0000000000000000000000000000000000000000..f5be9584072ef74316343707101c4e9b81b87e20 GIT binary patch literal 1083 zcmaJ=PfXKL7;hXJf*=G#qFi_>93;ASEnCMLCX9B#%(~2Ni92|(v=7!m|Ij{M;WY7} z7^Bg6A{sR@a^vEW7VbGL@nE8yO!QD+$2L4To4!Ba_rBln`@Z*mZ#|ot z?&|38APAx>H6!Nm+Ji@LdjQYy%T^IDCsDG9^5z^W%QhqusyPcmN|)zh4$A7v(rY+I z5P>1BP((#3%`2u($-WKc>J~;5#MroN$;tvmU>446Mx6Y<{elFV8YeGAq>y9@u%ykb z+AzPGDJZK8N=zlk&w??R#{@b=GH~@p!{OaHS>xsL-dEEksF|RJIJqBGQOW|rv>}L4 zBS9s^Fo5GICLG~7<_us%3`>Xb&jpzc7uj9*kBN$qu82-G}8b+qdaRaqd18p-A_TcnzV*f zzb1TPv@2UQLxud58itbee^Xs=T01BQ590kNu~S&FAf1Dbxoj)AaC3vcD~lIwC?nG@ znC4>R7PBQ2nNG>HfRN|FDM?l|!&eN~5R$~F3wf^&1' ) + .addClass( 'mw-widget-mediaResultWidget-thumbnail' ) + .on( { + load: this.onThumbnailLoad.bind( this ), + error: this.onThumbnailError.bind( this ) + } ); + this.$overlay = $( '
' ) + .addClass( 'mw-widget-mediaResultWidget-overlay' ); + + this.calculateSizing( this.data ); + + // Get wiki default thumbnail size + this.defaultThumbSize = mw.config.get( 'wgVisualEditorConfig' ) + .defaultUserOptions.defaultthumbsize; + + // Initialization + this.setLabel( new mw.Title( this.data.title ).getNameText() ); + this.$label.addClass( 'mw-widget-mediaResultWidget-nameLabel' ); + + this.$element + .addClass( 'mw-widget-mediaResultWidget ve-ui-texture-pending' ) + .prepend( this.$thumb, this.$overlay ); + }; + + /* Inheritance */ + + OO.inheritClass( mw.widgets.MediaResultWidget, OO.ui.OptionWidget ); + + /* Static methods */ + + // Copied from ve.dm.MWImageNode + mw.widgets.MediaResultWidget.static.resizeToBoundingBox = function ( imageDimensions, boundingBox ) { + var newDimensions = OO.copy( imageDimensions ), + scale = Math.min( + boundingBox.height / imageDimensions.height, + boundingBox.width / imageDimensions.width + ); + + if ( scale < 1 ) { + // Scale down + newDimensions = { + width: Math.floor( newDimensions.width * scale ), + height: Math.floor( newDimensions.height * scale ) + }; + } + return newDimensions; + }; + + /* Methods */ + /** */ + mw.widgets.MediaResultWidget.prototype.onThumbnailLoad = function () { + this.$thumb.first().addClass( 've-ui-texture-transparency' ); + this.$element + .addClass( 'mw-widget-mediaResultWidget-done' ) + .removeClass( 've-ui-texture-pending' ); + }; + + /** */ + mw.widgets.MediaResultWidget.prototype.onThumbnailError = function () { + this.$thumb.last() + .css( 'background-image', '' ) + .addClass( 've-ui-texture-alert' ); + this.$element + .addClass( 'mw-widget-mediaResultWidget-error' ) + .removeClass( 've-ui-texture-pending' ); + }; + + /** + * Resize the thumbnail and wrapper according to row height and bounding boxes, if given. + * + * @param {Object} originalDimensions Original image dimensions with width and height values + * @param {Object} [boundingBox] Specific bounding box, if supplied + */ + mw.widgets.MediaResultWidget.prototype.calculateSizing = function ( originalDimensions, boundingBox ) { + var wrapperPadding, + imageDimensions = {}; + + boundingBox = boundingBox || {}; + + if ( this.isAudio ) { + // HACK: We are getting the wrong information from the + // API about audio files. Set their thumbnail to square 120px + imageDimensions = { + width: 120, + height: 120 + }; + } else { + // Get the image within the bounding box + imageDimensions = this.constructor.static.resizeToBoundingBox( + // Image original dimensions + { + width: originalDimensions.width || originalDimensions.thumbwidth, + height: originalDimensions.height || originalDimensions.thumbwidth + }, + // Bounding box + { + width: boundingBox.width || this.getImageMaxWidth(), + height: boundingBox.height || this.getRowHeight() + } + ); + } + this.imageDimensions = imageDimensions; + // Set the thumbnail size + this.$thumb.css( this.imageDimensions ); + + // Set the box size + wrapperPadding = this.calculateWrapperPadding( this.imageDimensions ); + this.$element.css( wrapperPadding ); + }; + + /** + * Replace the empty .src attribute of the image with the + * actual src. + */ + mw.widgets.MediaResultWidget.prototype.lazyLoad = function () { + if ( !this.hasSrc() ) { + this.src = this.thumbUrl; + this.$thumb.attr( 'src', this.thumbUrl ); + } + }; + + /** + * Retrieve the store dimensions object + * + * @return {Object} Thumb dimensions + */ + mw.widgets.MediaResultWidget.prototype.getDimensions = function () { + return this.dimensions; + }; + + /** + * Resize thumbnail and element according to the resize factor + * + * @param {number} resizeFactor The resizing factor for the image + */ + mw.widgets.MediaResultWidget.prototype.resizeThumb = function ( resizeFactor ) { + var boundingBox, + imageOriginalWidth = this.imageDimensions.width, + wrapperWidth = this.$element.width(); + // Set the new row height + this.setRowHeight( Math.ceil( this.getRowHeight() * resizeFactor ) ); + + boundingBox = { + width: Math.ceil( this.imageDimensions.width * resizeFactor ), + height: this.getRowHeight() + }; + + this.calculateSizing( this.data, boundingBox ); + + // We need to adjust the wrapper this time to fit the "perfect" + // dimensions, regardless of how small the image is + if ( imageOriginalWidth < wrapperWidth ) { + boundingBox.width = wrapperWidth * resizeFactor; + } + this.$element.css( this.calculateWrapperPadding( boundingBox ) ); + }; + + /** + * Adjust the wrapper padding for small images + * + * @param {Object} thumbDimensions Thumbnail dimensions + * @return {Object} Css styling for the wrapper + */ + mw.widgets.MediaResultWidget.prototype.calculateWrapperPadding = function ( thumbDimensions ) { + var css = { + height: this.rowHeight, + width: thumbDimensions.width, + lineHeight: this.getRowHeight() + 'px' + }; + + // Check if the image is too thin so we can make a bit of space around it + if ( thumbDimensions.width < this.minWidth ) { + css.width = this.minWidth; + } + + return css; + }; + + /** + * Set the row height for all size calculations + * + * @return {number} rowHeight Row height + */ + mw.widgets.MediaResultWidget.prototype.getRowHeight = function () { + return this.rowHeight; + }; + + /** + * Set the row height for all size calculations + * + * @param {number} rowHeight Row height + */ + mw.widgets.MediaResultWidget.prototype.setRowHeight = function ( rowHeight ) { + this.rowHeight = rowHeight; + }; + + mw.widgets.MediaResultWidget.prototype.setImageMaxWidth = function ( width ) { + this.maxWidth = width; + }; + mw.widgets.MediaResultWidget.prototype.getImageMaxWidth = function () { + return this.maxWidth; + }; + + /** + * Set the row this result is in. + * + * @param {number} row Row number + */ + mw.widgets.MediaResultWidget.prototype.setRow = function ( row ) { + this.row = row; + }; + + /** + * Get the row this result is in. + * + * @return {number} row Row number + */ + mw.widgets.MediaResultWidget.prototype.getRow = function () { + return this.row; + }; + + /** + * Check if the image has a src attribute already + * + * @return {boolean} Thumbnail has its source attribute set + */ + mw.widgets.MediaResultWidget.prototype.hasSrc = function () { + return !!this.src; + }; +}( jQuery, mediaWiki ) ); diff --git a/resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaSearchProvider.js b/resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaSearchProvider.js new file mode 100644 index 0000000000..a46d911004 --- /dev/null +++ b/resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaSearchProvider.js @@ -0,0 +1,69 @@ +/*! + * MediaWiki Widgets - MediaSearchProvider class. + * + * @copyright 2011-2016 VisualEditor Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ +( function ( $, mw ) { + + /** + * MediaWiki media search provider. + * + * @class + * @extends mw.widgets.MediaResourceProvider + * + * @constructor + * @param {string} apiurl The API url + * @param {Object} [config] Configuration options + */ + mw.widgets.MediaSearchProvider = function MwWidgetsMediaSearchProvider( apiurl, config ) { + config = config || {}; + + config.staticParams = $.extend( { + generator: 'search', + gsrnamespace: mw.config.get( 'wgNamespaceIds' ).file + }, config.staticParams ); + + // Parent constructor + mw.widgets.MediaSearchProvider.super.call( this, apiurl, config ); + }; + + /* Inheritance */ + OO.inheritClass( mw.widgets.MediaSearchProvider, mw.widgets.MediaResourceProvider ); + + /* Methods */ + + /** + * @inheritdoc + */ + mw.widgets.MediaSearchProvider.prototype.getContinueData = function ( howMany ) { + return { + gsroffset: this.getOffset(), + gsrlimit: howMany || this.getDefaultFetchLimit() + }; + }; + + /** + * @inheritdoc + */ + mw.widgets.MediaSearchProvider.prototype.setContinue = function ( continueData ) { + // Update the offset for next time + this.setOffset( continueData.gsroffset ); + }; + + /** + * @inheritdoc + */ + mw.widgets.MediaSearchProvider.prototype.sort = function ( results ) { + return results.sort( function ( a, b ) { + return a.index - b.index; + } ); + }; + + /** + * @inheritdoc + */ + mw.widgets.MediaSearchProvider.prototype.isValid = function () { + return this.getUserParams().gsrsearch && mw.widgets.MediaSearchProvider.super.prototype.isValid.call( this ); + }; +}( jQuery, mediaWiki ) ); diff --git a/resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaSearchQueue.js b/resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaSearchQueue.js new file mode 100644 index 0000000000..7ee98bb66d --- /dev/null +++ b/resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaSearchQueue.js @@ -0,0 +1,82 @@ +/*! + * MediaWiki Widgets - MediaSearchQueue class. + * + * @copyright 2011-2016 VisualEditor Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ +( function ( $, mw ) { + + /** + * MediaWiki media resource queue. + * + * @class + * @extends mw.widgets.MediaResourceQueue + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {number} maxHeight The maximum height of the media, used in the + * search call to the API. + */ + mw.widgets.MediaSearchQueue = function MwWidgetsMediaSearchQueue( config ) { + config = config || {}; + + // Parent constructor + mw.widgets.MediaSearchQueue.super.call( this, config ); + + this.searchQuery = ''; + }; + + /* Inheritance */ + OO.inheritClass( mw.widgets.MediaSearchQueue, mw.widgets.MediaResourceQueue ); + + /** + * Override parent method to set up the providers according to + * the file repos + * + * @return {jQuery.Promise} Promise that resolves when the resources are set up + */ + mw.widgets.MediaSearchQueue.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 mw.widgets.MediaSearchProvider( + sources[ i ].apiurl, + { + name: sources[ i ].name, + local: sources[ i ].local, + scriptDirUrl: sources[ i ].scriptDirUrl, + userParams: { + gsrsearch: queue.getSearchQuery() + }, + staticParams: { + iiurlheight: queue.getMaxHeight() + } + } ) + ); + } + } + } ); + }; + + /** + * Set the search query + * + * @param {string} searchQuery API search query + */ + mw.widgets.MediaSearchQueue.prototype.setSearchQuery = function ( searchQuery ) { + this.setParams( { gsrsearch: searchQuery } ); + }; + + /** + * Get the search query + * + * @return {string} API search query + */ + mw.widgets.MediaSearchQueue.prototype.getSearchQuery = function () { + return this.getParams().gsrsearch; + }; +}( jQuery, mediaWiki ) ); diff --git a/resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaSearchWidget.css b/resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaSearchWidget.css new file mode 100644 index 0000000000..3d28ef8ff8 --- /dev/null +++ b/resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaSearchWidget.css @@ -0,0 +1,10 @@ +/*! + * MediaWiki Widgets - MediaSearchWidget styles. + * + * @copyright 2011-2016 VisualEditor Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ + +.mw-widget-mediaSearchWidget .oo-ui-searchWidget-query .oo-ui-inputWidget { + max-width: none; +} diff --git a/resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaSearchWidget.js b/resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaSearchWidget.js new file mode 100644 index 0000000000..c6938e874e --- /dev/null +++ b/resources/src/mediawiki.widgets/MediaSearch/mw.widgets.MediaSearchWidget.js @@ -0,0 +1,462 @@ +/*! + * MediaWiki Widgets - MediaSearchWidget class. + * + * @copyright 2011-2016 VisualEditor Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ +( function ( $, mw ) { + + /** + * Creates an mw.widgets.MediaSearchWidget object. + * + * @class + * @extends OO.ui.SearchWidget + * + * @constructor + * @param {Object} [config] Configuration options + * @param {number} [size] Vertical size of thumbnails + */ + mw.widgets.MediaSearchWidget = function MwWidgetsMediaSearchWidget( config ) { + // Configuration initialization + config = $.extend( { + placeholder: mw.msg( 'mw-widgets-mediasearch-input-placeholder' ) + }, config ); + + // Parent constructor + mw.widgets.MediaSearchWidget.super.call( this, config ); + + // Properties + this.providers = {}; + this.lastQueryValue = ''; + this.searchQueue = new mw.widgets.MediaSearchQueue( { + limit: this.constructor.static.limit, + threshold: this.constructor.static.threshold + } ); + + this.queryTimeout = null; + this.itemCache = {}; + this.promises = []; + this.lang = config.lang || 'en'; + this.$panels = config.$panels; + + this.externalLinkUrlProtocolsRegExp = new RegExp( + '^(' + mw.config.get( 'wgUrlProtocols' ) + ')', + 'i' + ); + + // Masonry fit properties + this.rows = []; + this.rowHeight = config.rowHeight || 200; + this.layoutQueue = []; + this.numItems = 0; + this.currentItemCache = []; + + this.resultsSize = {}; + + this.selected = null; + + this.noItemsMessage = new OO.ui.LabelWidget( { + label: mw.msg( 'mw-widgets-mediasearch-noresults' ), + classes: [ 'mw-widget-mediaSearchWidget-noresults' ] + } ); + this.noItemsMessage.toggle( false ); + + // Events + this.$results.on( 'scroll', this.onResultsScroll.bind( this ) ); + this.$query.append( this.noItemsMessage.$element ); + this.results.connect( this, { + add: 'onResultsAdd', + remove: 'onResultsRemove' + } ); + + this.resizeHandler = OO.ui.debounce( this.afterResultsResize.bind( this ), 500 ); + + // Initialization + this.$element.addClass( 'mw-widget-mediaSearchWidget' ); + }; + + /* Inheritance */ + + OO.inheritClass( mw.widgets.MediaSearchWidget, OO.ui.SearchWidget ); + + /* Static properties */ + + mw.widgets.MediaSearchWidget.static.limit = 10; + + mw.widgets.MediaSearchWidget.static.threshold = 5; + + /* Methods */ + + /** + * Respond to window resize and check if the result display should + * be updated. + */ + mw.widgets.MediaSearchWidget.prototype.afterResultsResize = function () { + var items = this.currentItemCache; + + if ( + items.length > 0 && + ( + this.resultsSize.width !== this.$results.width() || + this.resultsSize.height !== this.$results.height() + ) + ) { + this.resetRows(); + this.itemCache = {}; + this.processQueueResults( items ); + if ( this.results.getItems().length > 0 ) { + this.lazyLoadResults(); + } + + // Cache the size + this.resultsSize = { + width: this.$results.width(), + height: this.$results.height() + }; + } + }; + + /** + * Teardown the widget; disconnect the window resize event. + */ + mw.widgets.MediaSearchWidget.prototype.teardown = function () { + $( window ).off( 'resize', this.resizeHandler ); + }; + + /** + * Setup the widget; activate the resize event. + */ + mw.widgets.MediaSearchWidget.prototype.setup = function () { + $( window ).on( 'resize', this.resizeHandler ); + }; + + /** + * Query all sources for media. + * + * @method + */ + mw.widgets.MediaSearchWidget.prototype.queryMediaQueue = function () { + var search = this, + value = this.getQueryValue(); + + if ( value === '' ) { + return; + } + + this.query.pushPending(); + search.noItemsMessage.toggle( false ); + + this.searchQueue.setSearchQuery( value ); + this.searchQueue.get( this.constructor.static.limit ) + .then( function ( items ) { + if ( items.length > 0 ) { + search.processQueueResults( items ); + search.currentItemCache = search.currentItemCache.concat( items ); + } + + search.query.popPending(); + search.noItemsMessage.toggle( search.results.getItems().length === 0 ); + if ( search.results.getItems().length > 0 ) { + search.lazyLoadResults(); + } + + } ); + }; + + /** + * Process the media queue giving more items + * + * @method + * @param {Object[]} items Given items by the media queue + */ + mw.widgets.MediaSearchWidget.prototype.processQueueResults = function ( items ) { + var i, len, title, + resultWidgets = [], + inputSearchQuery = this.getQueryValue(), + queueSearchQuery = this.searchQueue.getSearchQuery(); + + if ( inputSearchQuery === '' || queueSearchQuery !== inputSearchQuery ) { + 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 mw.widgets.MediaResultWidget( { + data: items[ i ], + rowHeight: this.rowHeight, + maxWidth: this.results.$element.width() / 3, + minWidth: 30, + rowWidth: this.results.$element.width() + } ) + ); + } + } + this.results.addItems( resultWidgets ); + + }; + + /** + * Get the sanitized query value from the input + * + * @return {string} Query value + */ + mw.widgets.MediaSearchWidget.prototype.getQueryValue = function () { + var queryValue = this.query.getValue().trim(); + + if ( queryValue.match( this.externalLinkUrlProtocolsRegExp ) ) { + queryValue = queryValue.match( /.+\/([^\/]+)/ )[ 1 ]; + } + return queryValue; + }; + + /** + * Handle search value change + * + * @param {string} value New value + */ + mw.widgets.MediaSearchWidget.prototype.onQueryChange = function () { + // Get the sanitized query value + var queryValue = this.getQueryValue(); + + if ( queryValue === this.lastQueryValue ) { + return; + } + + // Parent method + mw.widgets.MediaSearchWidget.super.prototype.onQueryChange.apply( this, arguments ); + + // Reset + this.itemCache = {}; + this.currentItemCache = []; + this.resetRows(); + + // Empty the results queue + this.layoutQueue = []; + + // Change resource queue query + this.searchQueue.setSearchQuery( queryValue ); + this.lastQueryValue = queryValue; + + // Queue + clearTimeout( this.queryTimeout ); + this.queryTimeout = setTimeout( this.queryMediaQueue.bind( this ), 350 ); + }; + + /** + * Handle results scroll events. + * + * @param {jQuery.Event} e Scroll event + */ + mw.widgets.MediaSearchWidget.prototype.onResultsScroll = function () { + var position = this.$results.scrollTop() + this.$results.outerHeight(), + threshold = this.results.$element.outerHeight() - this.rowHeight * 3; + + // Check if we need to ask for more results + if ( !this.query.isPending() && position > threshold ) { + this.queryMediaQueue(); + } + + this.lazyLoadResults(); + }; + + /** + * Lazy-load the images that are visible. + */ + mw.widgets.MediaSearchWidget.prototype.lazyLoadResults = function () { + var i, elementTop, + items = this.results.getItems(), + resultsScrollTop = this.$results.scrollTop(), + position = resultsScrollTop + this.$results.outerHeight(); + + // Lazy-load results + for ( i = 0; i < items.length; i++ ) { + elementTop = items[ i ].$element.position().top; + if ( elementTop <= position && !items[ i ].hasSrc() ) { + // Load the image + items[ i ].lazyLoad(); + } + } + }; + + /** + * Reset all the rows; destroy the jQuery elements and reset + * the rows array. + */ + mw.widgets.MediaSearchWidget.prototype.resetRows = function () { + var i, len; + + for ( i = 0, len = this.rows.length; i < len; i++ ) { + this.rows[ i ].$element.remove(); + } + + this.rows = []; + this.itemCache = {}; + }; + + /** + * 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 + */ + mw.widgets.MediaSearchWidget.prototype.getAvailableRow = function () { + var row; + + if ( this.rows.length === 0 ) { + row = 0; + } else { + row = this.rows.length - 1; + } + + if ( !this.rows[ row ] ) { + // Create new row + this.rows[ row ] = { + isFull: false, + width: 0, + items: [], + $element: $( '
' ) + .addClass( 'mw-widget-mediaResultWidget-row' ) + .css( { + overflow: 'hidden' + } ) + .data( 'row', row ) + .attr( 'data-full', false ) + }; + // Append to results + this.results.$element.append( this.rows[ row ].$element ); + } else if ( this.rows[ row ].isFull ) { + row++; + // Create new row + this.rows[ row ] = { + isFull: false, + width: 0, + items: [], + $element: $( '
' ) + .addClass( 'mw-widget-mediaResultWidget-row' ) + .css( { + overflow: 'hidden' + } ) + .data( 'row', row ) + .attr( 'data-full', false ) + }; + // Append to results + this.results.$element.append( this.rows[ row ].$element ); + } + + return row; + }; + + /** + * Respond to add results event in the results widget. + * Override the way SelectWidget and GroupElement append the items + * into the group so we can append them in groups of rows. + * + * @param {mw.widgets.MediaResultWidget[]} items An array of item elements + */ + mw.widgets.MediaSearchWidget.prototype.onResultsAdd = function ( items ) { + var search = this; + + // Add method to a queue; this queue will only run when the widget + // is visible + this.layoutQueue.push( function () { + var i, j, ilen, jlen, itemWidth, row, effectiveWidth, + resizeFactor, + maxRowWidth = search.results.$element.width() - 15; + + // Go over the added items + row = search.getAvailableRow(); + for ( i = 0, ilen = items.length; i < ilen; i++ ) { + itemWidth = items[ i ].$element.outerWidth( true ); + + // Add items to row until it is full + if ( search.rows[ row ].width + itemWidth >= maxRowWidth ) { + // Mark this row as full + search.rows[ row ].isFull = true; + search.rows[ row ].$element.attr( 'data-full', true ); + + // Find the resize factor + effectiveWidth = search.rows[ row ].width; + resizeFactor = maxRowWidth / effectiveWidth; + + search.rows[ row ].$element.attr( 'data-effectiveWidth', effectiveWidth ); + search.rows[ row ].$element.attr( 'data-resizeFactor', resizeFactor ); + search.rows[ row ].$element.attr( 'data-row', row ); + + // Resize all images in the row to fit the width + for ( j = 0, jlen = search.rows[ row ].items.length; j < jlen; j++ ) { + search.rows[ row ].items[ j ].resizeThumb( resizeFactor ); + } + + // find another row + row = search.getAvailableRow(); + } + + // Add the cumulative + search.rows[ row ].width += itemWidth; + + // Store reference to the item and to the row + search.rows[ row ].items.push( items[ i ] ); + items[ i ].setRow( row ); + + // Append the item + search.rows[ row ].$element.append( items[ i ].$element ); + } + + // If we have less than 4 rows, call for more images + if ( search.rows.length < 4 ) { + search.queryMediaQueue(); + } + } ); + this.runLayoutQueue(); + }; + + /** + * Run layout methods from the queue only if the element is visible. + */ + mw.widgets.MediaSearchWidget.prototype.runLayoutQueue = function () { + var i, len; + + if ( this.$element.is( ':visible' ) ) { + for ( i = 0, len = this.layoutQueue.length; i < len; i++ ) { + this.layoutQueue.pop()(); + } + } + }; + + /** + * Respond to removing results event in the results widget. + * Clear the relevant rows. + * + * @param {OO.ui.OptionWidget[]} items Removed items + */ + mw.widgets.MediaSearchWidget.prototype.onResultsRemove = function ( items ) { + if ( items.length > 0 ) { + // In the case of the media search widget, if any items are removed + // all are removed (new search) + this.resetRows(); + this.currentItemCache = []; + } + }; + + /** + * Set language for the search results. + * + * @param {string} lang Language + */ + mw.widgets.MediaSearchWidget.prototype.setLang = function ( lang ) { + this.lang = lang; + }; + + /** + * Get language for the search results. + * + * @return {string} lang Language + */ + mw.widgets.MediaSearchWidget.prototype.getLang = function () { + return this.lang; + }; +}( jQuery, mediaWiki ) ); -- 2.20.1