/*! * VisualEditor UserInterface MWMediaSearchWidget class. * * @copyright 2011-2015 VisualEditor Team and others; see AUTHORS.txt * @license The MIT License (MIT); see LICENSE.txt */ /** * Creates an ve.ui.MWMediaSearchWidget object. * * @class * @extends OO.ui.SearchWidget * * @constructor * @param {Object} [config] Configuration options * @param {number} [size] Vertical size of thumbnails */ ve.ui.MWMediaSearchWidget = function VeUiMWMediaSearchWidget( config ) { // Configuration initialization config = ve.extendObject( { placeholder: ve.msg( 'visualeditor-media-input-placeholder' ) }, config ); // Parent constructor OO.ui.SearchWidget.call( this, config ); // Properties this.providers = {}; this.searchValue = ''; this.resourceQueue = new ve.dm.MWMediaResourceQueue( { limit: 20, threshold: 10 } ); this.queryTimeout = null; 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.selected = null; 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, { add: 'onResultsAdd', remove: 'onResultsRemove' } ); // Initialization this.$element.addClass( 've-ui-mwMediaSearchWidget' ); }; /* Inheritance */ OO.inheritClass( ve.ui.MWMediaSearchWidget, OO.ui.SearchWidget ); /* Methods */ /** * Query all sources for media. * * @method */ 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.setParams( { gsrsearch: 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(); } } ); }; /** * Process the media queue giving more items * * @method * @param {Object[]} items Given items by the media queue */ ve.ui.MWMediaSearchWidget.prototype.processQueueResults = function ( items ) { var i, len, title, resultWidgets = [], inputSearchQuery = this.query.getValue(), queueSearchQuery = this.resourceQueue.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 ve.ui.MWMediaResultWidget( { $: this.$, data: items[i], rowHeight: this.rowHeight, maxWidth: this.results.$element.width() / 3, minWidth: 30, rowWidth: this.results.$element.width() } ) ); } } this.results.addItems( resultWidgets ); }; /** * Handle search value change * * @param {string} value New value */ ve.ui.MWMediaSearchWidget.prototype.onQueryChange = function ( value ) { var trimmed = $.trim( value ); if ( trimmed === this.searchValue ) { return; } this.searchValue = trimmed; // Parent method OO.ui.SearchWidget.prototype.onQueryChange.apply( this, arguments ); // Reset this.itemCache = {}; this.resetRows(); // Empty the results queue this.layoutQueue = []; // Change resource queue query this.resourceQueue.setParams( { gsrsearch: this.searchValue } ); // Queue clearTimeout( this.queryTimeout ); this.queryTimeout = setTimeout( this.queryMediaQueueCallback, 350 ); }; /** * Handle results scroll events. * * @param {jQuery.Event} e Scroll event */ ve.ui.MWMediaSearchWidget.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. */ ve.ui.MWMediaSearchWidget.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. */ ve.ui.MWMediaSearchWidget.prototype.resetRows = function () { var i, len; for ( i = 0, len = this.rows.length; i < len; i++ ) { this.rows[i].$element.remove(); } this.rows = []; }; /** * 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 () { 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: this.$( '