2 * MediaWiki Widgets - MediaSearchWidget class.
4 * @copyright 2011-2016 VisualEditor Team and others; see AUTHORS.txt
5 * @license The MIT License (MIT); see LICENSE.txt
10 * Creates an mw.widgets.MediaSearchWidget object.
13 * @extends OO.ui.SearchWidget
16 * @param {Object} [config] Configuration options
17 * @param {number} [size] Vertical size of thumbnails
19 mw.widgets.MediaSearchWidget = function MwWidgetsMediaSearchWidget( config ) {
20 // Configuration initialization
22 placeholder: mw.msg( 'mw-widgets-mediasearch-input-placeholder' )
26 mw.widgets.MediaSearchWidget.super.call( this, config );
30 this.lastQueryValue = '';
31 this.searchQueue = new mw.widgets.MediaSearchQueue( {
32 limit: this.constructor.static.limit,
33 threshold: this.constructor.static.threshold
36 this.queryTimeout = null;
39 this.lang = config.lang || 'en';
40 this.$panels = config.$panels;
42 this.externalLinkUrlProtocolsRegExp = new RegExp(
43 '^(' + mw.config.get( 'wgUrlProtocols' ) + ')',
47 // Masonry fit properties
49 this.rowHeight = config.rowHeight || 200;
50 this.layoutQueue = [];
52 this.currentItemCache = [];
54 this.resultsSize = {};
58 this.noItemsMessage = new OO.ui.LabelWidget( {
59 label: mw.msg( 'mw-widgets-mediasearch-noresults' ),
60 classes: [ 'mw-widget-mediaSearchWidget-noresults' ]
62 this.noItemsMessage.toggle( false );
65 this.$results.on( 'scroll', this.onResultsScroll.bind( this ) );
66 this.$query.append( this.noItemsMessage.$element );
67 this.results.connect( this, {
69 remove: 'onResultsRemove'
72 this.resizeHandler = OO.ui.debounce( this.afterResultsResize.bind( this ), 500 );
75 this.$element.addClass( 'mw-widget-mediaSearchWidget' );
80 OO.inheritClass( mw.widgets.MediaSearchWidget, OO.ui.SearchWidget );
82 /* Static properties */
84 mw.widgets.MediaSearchWidget.static.limit = 10;
86 mw.widgets.MediaSearchWidget.static.threshold = 5;
91 * Respond to window resize and check if the result display should
94 mw.widgets.MediaSearchWidget.prototype.afterResultsResize = function () {
95 var items = this.currentItemCache;
100 this.resultsSize.width !== this.$results.width() ||
101 this.resultsSize.height !== this.$results.height()
106 this.processQueueResults( items );
107 if ( this.results.getItems().length > 0 ) {
108 this.lazyLoadResults();
113 width: this.$results.width(),
114 height: this.$results.height()
120 * Teardown the widget; disconnect the window resize event.
122 mw.widgets.MediaSearchWidget.prototype.teardown = function () {
123 $( window ).off( 'resize', this.resizeHandler );
127 * Setup the widget; activate the resize event.
129 mw.widgets.MediaSearchWidget.prototype.setup = function () {
130 $( window ).on( 'resize', this.resizeHandler );
134 * Query all sources for media.
138 mw.widgets.MediaSearchWidget.prototype.queryMediaQueue = function () {
140 value = this.getQueryValue();
142 if ( value === '' ) {
146 this.query.pushPending();
147 search.noItemsMessage.toggle( false );
149 this.searchQueue.setSearchQuery( value );
150 this.searchQueue.get( this.constructor.static.limit )
151 .then( function ( items ) {
152 if ( items.length > 0 ) {
153 search.processQueueResults( items );
154 search.currentItemCache = search.currentItemCache.concat( items );
157 search.query.popPending();
158 search.noItemsMessage.toggle( search.results.getItems().length === 0 );
159 if ( search.results.getItems().length > 0 ) {
160 search.lazyLoadResults();
167 * Process the media queue giving more items
170 * @param {Object[]} items Given items by the media queue
172 mw.widgets.MediaSearchWidget.prototype.processQueueResults = function ( items ) {
175 inputSearchQuery = this.getQueryValue(),
176 queueSearchQuery = this.searchQueue.getSearchQuery();
178 if ( inputSearchQuery === '' || queueSearchQuery !== inputSearchQuery ) {
182 for ( i = 0, len = items.length; i < len; i++ ) {
183 title = new mw.Title( items[ i ].title ).getMainText();
184 // Do not insert duplicates
185 if ( !Object.prototype.hasOwnProperty.call( this.itemCache, title ) ) {
186 this.itemCache[ title ] = true;
188 new mw.widgets.MediaResultWidget( {
190 rowHeight: this.rowHeight,
191 maxWidth: this.results.$element.width() / 3,
193 rowWidth: this.results.$element.width()
198 this.results.addItems( resultWidgets );
203 * Get the sanitized query value from the input
205 * @return {string} Query value
207 mw.widgets.MediaSearchWidget.prototype.getQueryValue = function () {
208 var queryValue = this.query.getValue().trim();
210 if ( queryValue.match( this.externalLinkUrlProtocolsRegExp ) ) {
211 queryValue = queryValue.match( /.+\/([^\/]+)/ )[ 1 ];
217 * Handle search value change
219 * @param {string} value New value
221 mw.widgets.MediaSearchWidget.prototype.onQueryChange = function () {
222 // Get the sanitized query value
223 var queryValue = this.getQueryValue();
225 if ( queryValue === this.lastQueryValue ) {
230 mw.widgets.MediaSearchWidget.super.prototype.onQueryChange.apply( this, arguments );
234 this.currentItemCache = [];
237 // Empty the results queue
238 this.layoutQueue = [];
240 // Change resource queue query
241 this.searchQueue.setSearchQuery( queryValue );
242 this.lastQueryValue = queryValue;
245 clearTimeout( this.queryTimeout );
246 this.queryTimeout = setTimeout( this.queryMediaQueue.bind( this ), 350 );
250 * Handle results scroll events.
252 * @param {jQuery.Event} e Scroll event
254 mw.widgets.MediaSearchWidget.prototype.onResultsScroll = function () {
255 var position = this.$results.scrollTop() + this.$results.outerHeight(),
256 threshold = this.results.$element.outerHeight() - this.rowHeight * 3;
258 // Check if we need to ask for more results
259 if ( !this.query.isPending() && position > threshold ) {
260 this.queryMediaQueue();
263 this.lazyLoadResults();
267 * Lazy-load the images that are visible.
269 mw.widgets.MediaSearchWidget.prototype.lazyLoadResults = function () {
271 items = this.results.getItems(),
272 resultsScrollTop = this.$results.scrollTop(),
273 position = resultsScrollTop + this.$results.outerHeight();
276 for ( i = 0; i < items.length; i++ ) {
277 elementTop = items[ i ].$element.position().top;
278 if ( elementTop <= position && !items[ i ].hasSrc() ) {
280 items[ i ].lazyLoad();
286 * Reset all the rows; destroy the jQuery elements and reset
289 mw.widgets.MediaSearchWidget.prototype.resetRows = function () {
292 for ( i = 0, len = this.rows.length; i < len; i++ ) {
293 this.rows[ i ].$element.remove();
301 * Find an available row at the end. Either we will need to create a new
302 * row or use the last available row if it isn't full.
304 * @return {number} Row index
306 mw.widgets.MediaSearchWidget.prototype.getAvailableRow = function () {
309 if ( this.rows.length === 0 ) {
312 row = this.rows.length - 1;
315 if ( !this.rows[ row ] ) {
321 $element: $( '<div>' )
322 .addClass( 'mw-widget-mediaResultWidget-row' )
327 .attr( 'data-full', false )
330 this.results.$element.append( this.rows[ row ].$element );
331 } else if ( this.rows[ row ].isFull ) {
338 $element: $( '<div>' )
339 .addClass( 'mw-widget-mediaResultWidget-row' )
344 .attr( 'data-full', false )
347 this.results.$element.append( this.rows[ row ].$element );
354 * Respond to add results event in the results widget.
355 * Override the way SelectWidget and GroupElement append the items
356 * into the group so we can append them in groups of rows.
358 * @param {mw.widgets.MediaResultWidget[]} items An array of item elements
360 mw.widgets.MediaSearchWidget.prototype.onResultsAdd = function ( items ) {
363 // Add method to a queue; this queue will only run when the widget
365 this.layoutQueue.push( function () {
366 var i, j, ilen, jlen, itemWidth, row, effectiveWidth,
368 maxRowWidth = search.results.$element.width() - 15;
370 // Go over the added items
371 row = search.getAvailableRow();
372 for ( i = 0, ilen = items.length; i < ilen; i++ ) {
373 itemWidth = items[ i ].$element.outerWidth( true );
375 // Add items to row until it is full
376 if ( search.rows[ row ].width + itemWidth >= maxRowWidth ) {
377 // Mark this row as full
378 search.rows[ row ].isFull = true;
379 search.rows[ row ].$element.attr( 'data-full', true );
381 // Find the resize factor
382 effectiveWidth = search.rows[ row ].width;
383 resizeFactor = maxRowWidth / effectiveWidth;
385 search.rows[ row ].$element.attr( 'data-effectiveWidth', effectiveWidth );
386 search.rows[ row ].$element.attr( 'data-resizeFactor', resizeFactor );
387 search.rows[ row ].$element.attr( 'data-row', row );
389 // Resize all images in the row to fit the width
390 for ( j = 0, jlen = search.rows[ row ].items.length; j < jlen; j++ ) {
391 search.rows[ row ].items[ j ].resizeThumb( resizeFactor );
395 row = search.getAvailableRow();
398 // Add the cumulative
399 search.rows[ row ].width += itemWidth;
401 // Store reference to the item and to the row
402 search.rows[ row ].items.push( items[ i ] );
403 items[ i ].setRow( row );
406 search.rows[ row ].$element.append( items[ i ].$element );
409 // If we have less than 4 rows, call for more images
410 if ( search.rows.length < 4 ) {
411 search.queryMediaQueue();
414 this.runLayoutQueue();
418 * Run layout methods from the queue only if the element is visible.
420 mw.widgets.MediaSearchWidget.prototype.runLayoutQueue = function () {
423 if ( this.$element.is( ':visible' ) ) {
424 for ( i = 0, len = this.layoutQueue.length; i < len; i++ ) {
425 this.layoutQueue.pop()();
431 * Respond to removing results event in the results widget.
432 * Clear the relevant rows.
434 * @param {OO.ui.OptionWidget[]} items Removed items
436 mw.widgets.MediaSearchWidget.prototype.onResultsRemove = function ( items ) {
437 if ( items.length > 0 ) {
438 // In the case of the media search widget, if any items are removed
439 // all are removed (new search)
441 this.currentItemCache = [];
446 * Set language for the search results.
448 * @param {string} lang Language
450 mw.widgets.MediaSearchWidget.prototype.setLang = function ( lang ) {
455 * Get language for the search results.
457 * @return {string} lang Language
459 mw.widgets.MediaSearchWidget.prototype.getLang = function () {
462 }( jQuery, mediaWiki ) );