/*! * VisualEditor UserInterface MWMediaSearchWidget class. * * @copyright 2011-2016 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.searchQueue = new ve.dm.MWMediaSearchQueue( { 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; // 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: 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' } ); this.resizeHandler = ve.debounce( this.afterResultsResize.bind( this ), 500 ); // Initialization this.$element.addClass( 've-ui-mwMediaSearchWidget' ); }; /* Inheritance */ OO.inheritClass( ve.ui.MWMediaSearchWidget, OO.ui.SearchWidget ); /* Static properties */ ve.ui.MWMediaSearchWidget.static.limit = 10; ve.ui.MWMediaSearchWidget.static.threshold = 5; /* Methods */ /** * Respond to window resize and check if the result display should * be updated. */ ve.ui.MWMediaSearchWidget.prototype.afterResultsResize = function () { var items = this.currentItemCache, value = this.query.getValue(); if ( items.length > 0 && ( this.resultsSize.width !== this.$results.width() || this.resultsSize.height !== this.$results.height() ) ) { this.resetRows(); this.itemCache = {}; this.processQueueResults( items, value ); 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. */ ve.ui.MWMediaSearchWidget.prototype.teardown = function () { $( window ).off( 'resize', this.resizeHandler ); }; /** * Setup the widget; activate the resize event. */ ve.ui.MWMediaSearchWidget.prototype.setup = function () { $( window ).on( 'resize', this.resizeHandler ); }; /** * 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.searchQueue.setSearchQuery( value ); this.searchQueue.get( this.constructor.static.limit ) .then( function ( items ) { if ( items.length > 0 ) { search.processQueueResults( items, value ); 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 */ ve.ui.MWMediaSearchWidget.prototype.processQueueResults = function ( items ) { var i, len, title, resultWidgets = [], inputSearchQuery = this.query.getValue(), 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 ve.ui.MWMediaResultWidget( { 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 = value.trim(); if ( trimmed === this.searchValue ) { return; } this.searchValue = trimmed; // Parent method OO.ui.SearchWidget.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( this.searchValue ); // Queue clearTimeout( this.queryTimeout ); this.queryTimeout = setTimeout( this.queryMediaQueue.bind( this ), 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 = []; 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 */ 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: $( '