mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-26 23:31:02 +00:00
85f8030053
It is alarming that jscs is not picking this up.. Change-Id: Ibd3b113ebfedede9fa45120c24f00623ec5ea2fb
456 lines
11 KiB
JavaScript
456 lines
11 KiB
JavaScript
/*!
|
|
* 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
|
|
ve.ui.MWMediaSearchWidget.super.call( this, config );
|
|
|
|
// Properties
|
|
this.providers = {};
|
|
this.lastQueryValue = '';
|
|
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;
|
|
|
|
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.
|
|
*/
|
|
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.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
|
|
*/
|
|
ve.ui.MWMediaSearchWidget.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 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 );
|
|
|
|
};
|
|
|
|
/**
|
|
* Get the sanitized query value from the input
|
|
*
|
|
* @return {string} Query value
|
|
*/
|
|
ve.ui.MWMediaSearchWidget.prototype.getQueryValue = function () {
|
|
var queryValue = this.query.getValue().trim();
|
|
|
|
if ( queryValue.match( ve.init.platform.getExternalLinkUrlProtocolsRegExp() ) ) {
|
|
queryValue = queryValue.match( /.+\/([^\/]+)/ )[ 1 ];
|
|
}
|
|
return queryValue;
|
|
};
|
|
|
|
/**
|
|
* Handle search value change
|
|
*
|
|
* @param {string} value New value
|
|
*/
|
|
ve.ui.MWMediaSearchWidget.prototype.onQueryChange = function () {
|
|
// Get the sanitized query value
|
|
var queryValue = this.getQueryValue();
|
|
|
|
if ( queryValue === this.lastQueryValue ) {
|
|
return;
|
|
}
|
|
|
|
// Parent method
|
|
ve.ui.MWMediaSearchWidget.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
|
|
*/
|
|
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: $( '<div>' )
|
|
.addClass( 've-ui-mwMediaResultWidget-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: $( '<div>' )
|
|
.addClass( 've-ui-mwMediaResultWidget-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 {ve.ui.MWMediaResultWidget[]} items An array of item elements
|
|
*/
|
|
ve.ui.MWMediaSearchWidget.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.
|
|
*/
|
|
ve.ui.MWMediaSearchWidget.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
|
|
*/
|
|
ve.ui.MWMediaSearchWidget.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
|
|
*/
|
|
ve.ui.MWMediaSearchWidget.prototype.setLang = function ( lang ) {
|
|
this.lang = lang;
|
|
};
|
|
|
|
/**
|
|
* Get language for the search results.
|
|
*
|
|
* @return {string} lang Language
|
|
*/
|
|
ve.ui.MWMediaSearchWidget.prototype.getLang = function () {
|
|
return this.lang;
|
|
};
|