Merge "Selenium: replace UserLoginPage with BlankPage where possible"
[lhc/web/wiklou.git] / resources / src / mediawiki.widgets / MediaSearch / mw.widgets.MediaSearchWidget.js
1 /*!
2 * MediaWiki Widgets - MediaSearchWidget class.
3 *
4 * @copyright 2011-2016 VisualEditor Team and others; see AUTHORS.txt
5 * @license The MIT License (MIT); see LICENSE.txt
6 */
7 ( function () {
8
9 /**
10 * Creates an mw.widgets.MediaSearchWidget object.
11 *
12 * @class
13 * @extends OO.ui.SearchWidget
14 *
15 * @constructor
16 * @param {Object} [config] Configuration options
17 * @param {number} [size] Vertical size of thumbnails
18 */
19 mw.widgets.MediaSearchWidget = function MwWidgetsMediaSearchWidget( config ) {
20 // Configuration initialization
21 config = $.extend( {
22 placeholder: mw.msg( 'mw-widgets-mediasearch-input-placeholder' )
23 }, config );
24
25 // Parent constructor
26 mw.widgets.MediaSearchWidget.super.call( this, config );
27
28 // Properties
29 this.providers = {};
30 this.lastQueryValue = '';
31 this.searchQueue = new mw.widgets.MediaSearchQueue( {
32 limit: this.constructor.static.limit,
33 threshold: this.constructor.static.threshold
34 } );
35
36 this.queryTimeout = null;
37 this.itemCache = {};
38 this.promises = [];
39 this.lang = config.lang || 'en';
40 this.$panels = config.$panels;
41
42 this.externalLinkUrlProtocolsRegExp = new RegExp(
43 '^(' + mw.config.get( 'wgUrlProtocols' ) + ')',
44 'i'
45 );
46
47 // Masonry fit properties
48 this.rows = [];
49 this.rowHeight = config.rowHeight || 200;
50 this.layoutQueue = [];
51 this.numItems = 0;
52 this.currentItemCache = [];
53
54 this.resultsSize = {};
55
56 this.selected = null;
57
58 this.noItemsMessage = new OO.ui.LabelWidget( {
59 label: mw.msg( 'mw-widgets-mediasearch-noresults' ),
60 classes: [ 'mw-widget-mediaSearchWidget-noresults' ]
61 } );
62 this.noItemsMessage.toggle( false );
63
64 // Events
65 this.$results.on( 'scroll', this.onResultsScroll.bind( this ) );
66 this.$query.append( this.noItemsMessage.$element );
67 this.results.connect( this, {
68 change: 'onResultsChange',
69 remove: 'onResultsRemove'
70 } );
71
72 this.resizeHandler = OO.ui.debounce( this.afterResultsResize.bind( this ), 500 );
73
74 // Initialization
75 this.$element.addClass( 'mw-widget-mediaSearchWidget' );
76 };
77
78 /* Inheritance */
79
80 OO.inheritClass( mw.widgets.MediaSearchWidget, OO.ui.SearchWidget );
81
82 /* Static properties */
83
84 mw.widgets.MediaSearchWidget.static.limit = 10;
85
86 mw.widgets.MediaSearchWidget.static.threshold = 5;
87
88 /* Methods */
89
90 /**
91 * Respond to window resize and check if the result display should
92 * be updated.
93 */
94 mw.widgets.MediaSearchWidget.prototype.afterResultsResize = function () {
95 var items = this.currentItemCache;
96
97 if (
98 items.length > 0 &&
99 (
100 this.resultsSize.width !== this.$results.width() ||
101 this.resultsSize.height !== this.$results.height()
102 )
103 ) {
104 this.resetRows();
105 this.itemCache = {};
106 this.processQueueResults( items );
107 if ( this.results.getItems().length > 0 ) {
108 this.lazyLoadResults();
109 }
110
111 // Cache the size
112 this.resultsSize = {
113 width: this.$results.width(),
114 height: this.$results.height()
115 };
116 }
117 };
118
119 /**
120 * Teardown the widget; disconnect the window resize event.
121 */
122 mw.widgets.MediaSearchWidget.prototype.teardown = function () {
123 $( window ).off( 'resize', this.resizeHandler );
124 };
125
126 /**
127 * Setup the widget; activate the resize event.
128 */
129 mw.widgets.MediaSearchWidget.prototype.setup = function () {
130 $( window ).on( 'resize', this.resizeHandler );
131 };
132
133 /**
134 * Query all sources for media.
135 *
136 * @method
137 */
138 mw.widgets.MediaSearchWidget.prototype.queryMediaQueue = function () {
139 var search = this,
140 value = this.getQueryValue();
141
142 if ( value === '' ) {
143 return;
144 }
145
146 this.query.pushPending();
147 search.noItemsMessage.toggle( false );
148
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 );
155 }
156
157 search.query.popPending();
158 search.noItemsMessage.toggle( search.results.getItems().length === 0 );
159 if ( search.results.getItems().length > 0 ) {
160 search.lazyLoadResults();
161 }
162
163 } );
164 };
165
166 /**
167 * Process the media queue giving more items
168 *
169 * @method
170 * @param {Object[]} items Given items by the media queue
171 */
172 mw.widgets.MediaSearchWidget.prototype.processQueueResults = function ( items ) {
173 var i, len, title,
174 resultWidgets = [],
175 inputSearchQuery = this.getQueryValue(),
176 queueSearchQuery = this.searchQueue.getSearchQuery();
177
178 if ( inputSearchQuery === '' || queueSearchQuery !== inputSearchQuery ) {
179 return;
180 }
181
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;
187 resultWidgets.push(
188 new mw.widgets.MediaResultWidget( {
189 data: items[ i ],
190 rowHeight: this.rowHeight,
191 maxWidth: this.results.$element.width() / 3,
192 minWidth: 30,
193 rowWidth: this.results.$element.width()
194 } )
195 );
196 }
197 }
198 this.results.addItems( resultWidgets );
199
200 };
201
202 /**
203 * Get the sanitized query value from the input
204 *
205 * @return {string} Query value
206 */
207 mw.widgets.MediaSearchWidget.prototype.getQueryValue = function () {
208 var queryValue = this.query.getValue().trim();
209
210 if ( queryValue.match( this.externalLinkUrlProtocolsRegExp ) ) {
211 queryValue = queryValue.match( /.+\/([^/]+)/ )[ 1 ];
212 }
213 return queryValue;
214 };
215
216 /**
217 * Handle search value change
218 *
219 * @param {string} value New value
220 */
221 mw.widgets.MediaSearchWidget.prototype.onQueryChange = function () {
222 // Get the sanitized query value
223 var queryValue = this.getQueryValue();
224
225 if ( queryValue === this.lastQueryValue ) {
226 return;
227 }
228
229 // Parent method
230 mw.widgets.MediaSearchWidget.super.prototype.onQueryChange.apply( this, arguments );
231
232 // Reset
233 this.itemCache = {};
234 this.currentItemCache = [];
235 this.resetRows();
236
237 // Empty the results queue
238 this.layoutQueue = [];
239
240 // Change resource queue query
241 this.searchQueue.setSearchQuery( queryValue );
242 this.lastQueryValue = queryValue;
243
244 // Queue
245 clearTimeout( this.queryTimeout );
246 this.queryTimeout = setTimeout( this.queryMediaQueue.bind( this ), 350 );
247 };
248
249 /**
250 * Handle results scroll events.
251 *
252 * @param {jQuery.Event} e Scroll event
253 */
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;
257
258 // Check if we need to ask for more results
259 if ( !this.query.isPending() && position > threshold ) {
260 this.queryMediaQueue();
261 }
262
263 this.lazyLoadResults();
264 };
265
266 /**
267 * Lazy-load the images that are visible.
268 */
269 mw.widgets.MediaSearchWidget.prototype.lazyLoadResults = function () {
270 var i, elementTop,
271 items = this.results.getItems(),
272 resultsScrollTop = this.$results.scrollTop(),
273 position = resultsScrollTop + this.$results.outerHeight();
274
275 // Lazy-load results
276 for ( i = 0; i < items.length; i++ ) {
277 elementTop = items[ i ].$element.position().top;
278 if ( elementTop <= position && !items[ i ].hasSrc() ) {
279 // Load the image
280 items[ i ].lazyLoad();
281 }
282 }
283 };
284
285 /**
286 * Reset all the rows; destroy the jQuery elements and reset
287 * the rows array.
288 */
289 mw.widgets.MediaSearchWidget.prototype.resetRows = function () {
290 var i, len;
291
292 for ( i = 0, len = this.rows.length; i < len; i++ ) {
293 this.rows[ i ].$element.remove();
294 }
295
296 this.rows = [];
297 this.itemCache = {};
298 };
299
300 /**
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.
303 *
304 * @return {number} Row index
305 */
306 mw.widgets.MediaSearchWidget.prototype.getAvailableRow = function () {
307 var row;
308
309 if ( this.rows.length === 0 ) {
310 row = 0;
311 } else {
312 row = this.rows.length - 1;
313 }
314
315 if ( !this.rows[ row ] ) {
316 // Create new row
317 this.rows[ row ] = {
318 isFull: false,
319 width: 0,
320 items: [],
321 $element: $( '<div>' )
322 .addClass( 'mw-widget-mediaResultWidget-row' )
323 .css( {
324 overflow: 'hidden'
325 } )
326 .data( 'row', row )
327 .attr( 'data-full', false )
328 };
329 // Append to results
330 this.results.$element.append( this.rows[ row ].$element );
331 } else if ( this.rows[ row ].isFull ) {
332 row++;
333 // Create new row
334 this.rows[ row ] = {
335 isFull: false,
336 width: 0,
337 items: [],
338 $element: $( '<div>' )
339 .addClass( 'mw-widget-mediaResultWidget-row' )
340 .css( {
341 overflow: 'hidden'
342 } )
343 .data( 'row', row )
344 .attr( 'data-full', false )
345 };
346 // Append to results
347 this.results.$element.append( this.rows[ row ].$element );
348 }
349
350 return row;
351 };
352
353 /**
354 * Respond to change 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.
357 *
358 * @param {mw.widgets.MediaResultWidget[]} items An array of item elements
359 */
360 mw.widgets.MediaSearchWidget.prototype.onResultsChange = function ( items ) {
361 var search = this;
362
363 if ( !items.length ) {
364 return;
365 }
366
367 // Add method to a queue; this queue will only run when the widget
368 // is visible
369 this.layoutQueue.push( function () {
370 var i, j, ilen, jlen, itemWidth, row, effectiveWidth,
371 resizeFactor,
372 maxRowWidth = search.results.$element.width() - 15;
373
374 // Go over the added items
375 row = search.getAvailableRow();
376 for ( i = 0, ilen = items.length; i < ilen; i++ ) {
377
378 // Check item has just been added
379 if ( items[ i ].row !== null ) {
380 continue;
381 }
382
383 itemWidth = items[ i ].$element.outerWidth( true );
384
385 // Add items to row until it is full
386 if ( search.rows[ row ].width + itemWidth >= maxRowWidth ) {
387 // Mark this row as full
388 search.rows[ row ].isFull = true;
389 search.rows[ row ].$element.attr( 'data-full', true );
390
391 // Find the resize factor
392 effectiveWidth = search.rows[ row ].width;
393 resizeFactor = maxRowWidth / effectiveWidth;
394
395 search.rows[ row ].$element.attr( 'data-effectiveWidth', effectiveWidth );
396 search.rows[ row ].$element.attr( 'data-resizeFactor', resizeFactor );
397 search.rows[ row ].$element.attr( 'data-row', row );
398
399 // Resize all images in the row to fit the width
400 for ( j = 0, jlen = search.rows[ row ].items.length; j < jlen; j++ ) {
401 search.rows[ row ].items[ j ].resizeThumb( resizeFactor );
402 }
403
404 // find another row
405 row = search.getAvailableRow();
406 }
407
408 // Add the cumulative
409 search.rows[ row ].width += itemWidth;
410
411 // Store reference to the item and to the row
412 search.rows[ row ].items.push( items[ i ] );
413 items[ i ].setRow( row );
414
415 // Append the item
416 search.rows[ row ].$element.append( items[ i ].$element );
417
418 }
419
420 // If we have less than 4 rows, call for more images
421 if ( search.rows.length < 4 ) {
422 search.queryMediaQueue();
423 }
424 } );
425 this.runLayoutQueue();
426 };
427
428 /**
429 * Run layout methods from the queue only if the element is visible.
430 */
431 mw.widgets.MediaSearchWidget.prototype.runLayoutQueue = function () {
432 var i, len;
433
434 // eslint-disable-next-line no-jquery/no-sizzle
435 if ( this.$element.is( ':visible' ) ) {
436 for ( i = 0, len = this.layoutQueue.length; i < len; i++ ) {
437 this.layoutQueue.pop()();
438 }
439 }
440 };
441
442 /**
443 * Respond to removing results event in the results widget.
444 * Clear the relevant rows.
445 *
446 * @param {OO.ui.OptionWidget[]} items Removed items
447 */
448 mw.widgets.MediaSearchWidget.prototype.onResultsRemove = function ( items ) {
449 if ( items.length > 0 ) {
450 // In the case of the media search widget, if any items are removed
451 // all are removed (new search)
452 this.resetRows();
453 this.currentItemCache = [];
454 }
455 };
456
457 /**
458 * Set language for the search results.
459 *
460 * @param {string} lang Language
461 */
462 mw.widgets.MediaSearchWidget.prototype.setLang = function ( lang ) {
463 this.lang = lang;
464 };
465
466 /**
467 * Get language for the search results.
468 *
469 * @return {string} lang Language
470 */
471 mw.widgets.MediaSearchWidget.prototype.getLang = function () {
472 return this.lang;
473 };
474 }() );