MediaWiki:Gadget-VEBulkUpload.js
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
* VEBulkUpload — bulk file upload for VisualEditor.
*
* Drag a group of files anywhere onto the page you are visually editing (or use
* Insert → "Upload files"), give them a shared description/categories, and they
* are uploaded and inserted as a <gallery>, as thumbnails, or as a link list.
*
* Single-file drops are deliberately NOT intercepted — those keep going to
* VisualEditor's own media dialog (Extension:EnhancedUpload).
*
* Loaded into VisualEditor through $wgVisualEditorPluginModules, so this file
* executes before the editor toolbar is built.
*
* Documentation: [[Help:Uploading files]]
* Maintainer note: only three VisualEditor APIs are used — ve.ui.toolFactory,
* ve.ui.dataTransferHandlerFactory/DataTransferItem, and the ce.Surface drop
* events. Re-check those after a MediaWiki upgrade.
*/
( function () {
'use strict';
if ( typeof ve === 'undefined' || !ve.ui || !ve.ui.FragmentDialog || !ve.ui.FragmentWindowTool ) {
// Not inside VisualEditor — nothing to do.
return;
}
var IMAGE_EXTENSIONS = [ 'jpg', 'jpeg', 'png', 'gif', 'svg', 'webp', 'tif', 'tiff' ],
TITLE_QUERY_CHUNK = 45,
api = null,
allowedExtensionsPromise = null;
function getApi() {
if ( !api ) {
api = new mw.Api();
}
return api;
}
function getExtension( name ) {
var parts = String( name ).split( '.' );
return parts.length > 1 ? parts.pop().toLowerCase() : '';
}
function isImageName( name ) {
return IMAGE_EXTENSIONS.indexOf( getExtension( name ) ) !== -1;
}
function stripExtension( name ) {
return String( name ).replace( /\.[^.]+$/, '' );
}
function captionFor( name ) {
return stripExtension( name ).replace( /[_-]+/g, ' ' ).replace( /\s+/g, ' ' ).trim();
}
/**
* Turn a local filename into a valid File: page name.
*
* @param {string} name
* @return {string}
*/
function normaliseFileName( name ) {
var title = mw.Title.newFromFileName( name );
return title ? title.getMainText() : String( name ).replace( /[#<>[\]|{}]/g, '' );
}
/**
* Extensions this wiki accepts, from siteinfo. Cached for the session.
* Fails open (empty list = accept everything) so a flaky API can't block uploads.
*
* @return {jQuery.Promise} Promise resolving with an array of lowercase extensions
*/
function getAllowedExtensions() {
if ( !allowedExtensionsPromise ) {
allowedExtensionsPromise = getApi().get( {
action: 'query',
meta: 'siteinfo',
siprop: 'fileextensions',
formatversion: 2
} ).then( function ( data ) {
return ( data.query.fileextensions || [] ).map( function ( e ) {
return String( e.ext ).toLowerCase();
} );
}, function () {
return [];
} );
}
return allowedExtensionsPromise;
}
function apiErrorMessage( code, result ) {
if ( result && result.errors && result.errors[ 0 ] ) {
return result.errors[ 0 ].html || result.errors[ 0 ].text || code;
}
if ( result && result.error && result.error.info ) {
return result.error.info;
}
return String( code || 'unknown error' );
}
/* ---------------------------------------------------------------- rows -- */
/**
* One queued file in the dialog.
*
* @class
* @param {File} file
* @param {Object} [config]
*/
function FileRow( file, config ) {
var row = this;
FileRow.super.call( this, config || {} );
this.file = file;
this.exists = false;
this.done = false;
this.uploadedName = null;
this.error = null;
this.nameInput = new OO.ui.TextInputWidget( {
value: normaliseFileName( file.name ),
classes: [ 've-bulkupload-row-name' ]
} );
this.nameInput.connect( this, { change: 'onNameChange' } );
this.policyInput = new OO.ui.DropdownInputWidget( {
options: [
{ data: 'rename', label: 'Upload under a new name' },
{ data: 'existing', label: 'Use the file already on the wiki' },
{ data: 'overwrite', label: 'Overwrite the existing file' },
{ data: 'skip', label: 'Skip this file' }
],
value: 'rename',
classes: [ 've-bulkupload-row-policy' ]
} );
this.policyInput.toggle( false );
this.statusLabel = new OO.ui.LabelWidget( {
label: '',
classes: [ 've-bulkupload-row-status' ]
} );
this.removeButton = new OO.ui.ButtonWidget( {
icon: 'trash',
framed: false,
title: 'Remove from this upload',
classes: [ 've-bulkupload-row-remove' ]
} );
this.removeButton.connect( this, { click: 'onRemove' } );
this.$thumb = $( '<div>' ).addClass( 've-bulkupload-row-thumb' );
if ( isImageName( file.name ) && window.URL && URL.createObjectURL ) {
this.objectUrl = URL.createObjectURL( file );
this.$thumb.append( $( '<img>' ).attr( 'src', this.objectUrl ).on( 'error', function () {
row.$thumb.empty().text( getExtension( file.name ).toUpperCase() );
} ) );
} else {
this.$thumb.addClass( 've-bulkupload-row-thumb-generic' )
.text( getExtension( file.name ).toUpperCase() || '?' );
}
this.progressBar = new OO.ui.ProgressBarWidget( { progress: 0 } );
this.progressBar.toggle( false );
this.$element
.addClass( 've-bulkupload-row' )
.append(
this.$thumb,
$( '<div>' ).addClass( 've-bulkupload-row-body' ).append(
this.nameInput.$element,
$( '<div>' ).addClass( 've-bulkupload-row-meta' ).append(
$( '<span>' ).addClass( 've-bulkupload-row-size' )
.text( formatSize( file.size ) ),
this.statusLabel.$element,
this.policyInput.$element
),
this.progressBar.$element
),
this.removeButton.$element
);
}
OO.inheritClass( FileRow, OO.ui.Widget );
function formatSize( bytes ) {
if ( bytes > 1048576 ) {
return ( bytes / 1048576 ).toFixed( 1 ) + ' MB';
}
return Math.max( 1, Math.round( bytes / 1024 ) ) + ' KB';
}
FileRow.prototype.onRemove = function () {
this.emit( 'remove', this );
};
FileRow.prototype.onNameChange = function () {
this.emit( 'nameChange', this );
};
FileRow.prototype.getFileName = function () {
return normaliseFileName( this.nameInput.getValue().trim() );
};
FileRow.prototype.getPolicy = function () {
return this.exists ? this.policyInput.getValue() : 'rename';
};
FileRow.prototype.setExists = function ( exists ) {
this.exists = !!exists;
this.policyInput.toggle( this.exists );
this.setStatus( this.exists ? 'A file with this name already exists.' : '', this.exists ? 'warning' : null );
};
FileRow.prototype.setStatus = function ( text, kind ) {
this.statusLabel.setLabel( text || '' );
this.statusLabel.$element
.toggleClass( 've-bulkupload-status-warning', kind === 'warning' )
.toggleClass( 've-bulkupload-status-error', kind === 'error' )
.toggleClass( 've-bulkupload-status-done', kind === 'done' );
};
FileRow.prototype.setProgress = function ( fraction ) {
this.progressBar.toggle( true );
this.progressBar.setProgress( Math.round( fraction * 100 ) );
};
FileRow.prototype.setBusy = function ( busy ) {
this.nameInput.setDisabled( busy );
this.policyInput.setDisabled( busy );
this.removeButton.setDisabled( busy );
};
FileRow.prototype.destroy = function () {
if ( this.objectUrl ) {
URL.revokeObjectURL( this.objectUrl );
this.objectUrl = null;
}
};
/* -------------------------------------------------------------- dialog -- */
/**
* @class
* @param {Object} [config]
*/
function BulkUploadDialog( config ) {
BulkUploadDialog.super.call( this, config );
}
OO.inheritClass( BulkUploadDialog, ve.ui.FragmentDialog );
BulkUploadDialog.static.name = 'veBulkUpload';
BulkUploadDialog.static.title = 'Upload files';
BulkUploadDialog.static.size = 'large';
BulkUploadDialog.static.actions = [
{
action: 'upload',
label: 'Upload and insert',
flags: [ 'primary', 'progressive' ],
modes: 'edit'
},
{
label: 'Cancel',
flags: [ 'safe', 'close' ],
modes: 'edit'
},
{
action: 'abort',
label: 'Stop',
flags: [ 'safe' ],
modes: 'uploading'
},
{
action: 'finish',
label: 'Close',
flags: [ 'primary', 'progressive' ],
modes: 'result'
}
];
BulkUploadDialog.prototype.initialize = function () {
var dialog = this;
BulkUploadDialog.super.prototype.initialize.call( this );
this.panel = new OO.ui.PanelLayout( { padded: true, expanded: false } );
// Drop zone / file picker
this.$fileInput = $( '<input>' )
.attr( { type: 'file', multiple: 'multiple' } )
.addClass( 've-bulkupload-fileinput' )
.on( 'change', function () {
dialog.addFiles( Array.prototype.slice.call( this.files ) );
this.value = '';
} );
this.$dropZone = $( '<div>' )
.addClass( 've-bulkupload-dropzone' )
.append(
$( '<p>' ).text( 'Drop files here, or' ),
new OO.ui.ButtonWidget( {
label: 'Choose files',
icon: 'upload',
flags: [ 'progressive' ]
} ).on( 'click', function () {
dialog.$fileInput.trigger( 'click' );
} ).$element,
this.$fileInput
)
.on( 'dragover dragenter', function ( e ) {
e.preventDefault();
e.stopPropagation();
$( this ).addClass( 've-bulkupload-dropzone-active' );
} )
.on( 'dragleave dragend', function () {
$( this ).removeClass( 've-bulkupload-dropzone-active' );
} )
.on( 'drop', function ( e ) {
e.preventDefault();
e.stopPropagation();
$( this ).removeClass( 've-bulkupload-dropzone-active' );
var files = e.originalEvent.dataTransfer && e.originalEvent.dataTransfer.files;
if ( files && files.length ) {
dialog.addFiles( Array.prototype.slice.call( files ) );
}
} );
this.$fileList = $( '<div>' ).addClass( 've-bulkupload-files' );
// Shared options
this.insertAsInput = new OO.ui.RadioSelectInputWidget( {
options: [
{ data: 'gallery', label: 'A gallery' },
{ data: 'thumbs', label: 'Individual thumbnails' },
{ data: 'links', label: 'A list of file links' }
],
value: 'gallery'
} );
this.insertAsInput.connect( this, { change: 'onInsertAsChange' } );
this.heightsInput = new OO.ui.NumberInputWidget( { value: 180, min: 60, max: 500 } );
this.captionsInput = new OO.ui.CheckboxInputWidget( { selected: true } );
this.descriptionInput = new OO.ui.MultilineTextInputWidget( {
rows: 2,
placeholder: 'Shared description saved on every file page (optional)'
} );
this.categoriesInput = new mw.widgets.CategoryMultiselectWidget();
this.galleryField = new OO.ui.FieldLayout( this.heightsInput, {
label: 'Gallery image height (px)',
align: 'top'
} );
this.optionsFieldset = new OO.ui.FieldsetLayout( { label: 'Insert as' } );
this.optionsFieldset.addItems( [
new OO.ui.FieldLayout( this.insertAsInput, { align: 'top' } ),
this.galleryField,
new OO.ui.FieldLayout( this.captionsInput, {
label: 'Use file names as captions',
align: 'inline'
} ),
new OO.ui.FieldLayout( this.descriptionInput, {
label: 'Description',
align: 'top'
} ),
new OO.ui.FieldLayout( this.categoriesInput, {
label: 'Categories for every file',
align: 'top'
} )
] );
// Progress + results
this.overallProgress = new OO.ui.ProgressBarWidget( { progress: 0 } );
this.overallProgress.toggle( false );
this.statusLabel = new OO.ui.LabelWidget( {
label: '',
classes: [ 've-bulkupload-status' ]
} );
this.$results = $( '<div>' ).addClass( 've-bulkupload-results' ).hide();
this.panel.$element.append(
this.$dropZone,
this.$fileList,
this.optionsFieldset.$element,
this.overallProgress.$element,
this.statusLabel.$element,
this.$results
);
this.$body.append( this.panel.$element );
};
BulkUploadDialog.prototype.onInsertAsChange = function () {
this.galleryField.toggle( this.insertAsInput.getValue() === 'gallery' );
};
BulkUploadDialog.prototype.getSetupProcess = function ( data ) {
data = data || {};
return BulkUploadDialog.super.prototype.getSetupProcess.call( this, data )
.next( function () {
this.rows = [];
this.results = [];
this.failures = [];
this.aborted = false;
this.currentUpload = null;
this.$fileList.empty();
this.$results.empty().hide();
this.overallProgress.toggle( false ).setProgress( 0 );
this.statusLabel.setLabel( '' );
this.descriptionInput.setValue( '' );
this.categoriesInput.setValue( [] );
this.insertAsInput.setValue( 'gallery' );
this.captionsInput.setSelected( true );
this.onInsertAsChange();
this.actions.setMode( 'edit' );
if ( data.files && data.files.length ) {
this.addFiles( data.files );
}
this.updateActions();
}, this );
};
BulkUploadDialog.prototype.getTeardownProcess = function ( data ) {
return BulkUploadDialog.super.prototype.getTeardownProcess.call( this, data )
.first( function () {
this.rows.forEach( function ( row ) {
row.destroy();
} );
this.rows = [];
}, this );
};
BulkUploadDialog.prototype.addFiles = function ( files ) {
var dialog = this;
getAllowedExtensions().then( function ( allowed ) {
var rejected = [];
files.forEach( function ( file ) {
var ext = getExtension( file.name );
if ( allowed.length && allowed.indexOf( ext ) === -1 ) {
rejected.push( file.name );
return;
}
var row = new FileRow( file );
row.connect( dialog, { remove: 'onRowRemove', nameChange: 'onRowNameChange' } );
dialog.rows.push( row );
dialog.$fileList.append( row.$element );
} );
if ( rejected.length ) {
dialog.statusLabel.setLabel(
'Not allowed on this wiki, skipped: ' + rejected.join( ', ' )
);
}
dialog.updateActions();
dialog.checkExisting();
dialog.updateSize();
} );
};
BulkUploadDialog.prototype.onRowRemove = function ( row ) {
var index = this.rows.indexOf( row );
if ( index !== -1 ) {
this.rows.splice( index, 1 );
row.$element.remove();
row.destroy();
this.updateActions();
this.updateSize();
}
};
BulkUploadDialog.prototype.onRowNameChange = function () {
if ( this.nameCheckTimeout ) {
clearTimeout( this.nameCheckTimeout );
}
this.nameCheckTimeout = setTimeout( this.checkExisting.bind( this ), 500 );
};
BulkUploadDialog.prototype.updateActions = function () {
this.actions.setAbilities( { upload: this.rows.length > 0 } );
};
/**
* Ask the wiki which of the queued names are already taken, so conflicts are
* resolved before a long upload run rather than in the middle of one.
*/
BulkUploadDialog.prototype.checkExisting = function () {
var dialog = this,
rows = this.rows.slice(),
titles = rows.map( function ( row ) {
return 'File:' + row.getFileName();
} ),
chunks = [],
i;
if ( !titles.length ) {
return;
}
for ( i = 0; i < titles.length; i += TITLE_QUERY_CHUNK ) {
chunks.push( titles.slice( i, i + TITLE_QUERY_CHUNK ) );
}
$.when.apply( $, chunks.map( function ( chunk ) {
return getApi().get( {
action: 'query',
titles: chunk,
formatversion: 2
} );
} ) ).then( function () {
var existing = {};
Array.prototype.slice.call( arguments ).forEach( function ( response ) {
var data = Array.isArray( response ) ? response[ 0 ] : response;
( ( data && data.query && data.query.pages ) || [] ).forEach( function ( page ) {
if ( !page.missing ) {
existing[ page.title ] = true;
}
} );
} );
rows.forEach( function ( row ) {
if ( dialog.rows.indexOf( row ) !== -1 && !row.done ) {
row.setExists( !!existing[ 'File:' + row.getFileName() ] );
}
} );
dialog.updateSize();
} );
};
BulkUploadDialog.prototype.getActionProcess = function ( action ) {
var dialog = this;
if ( action === 'upload' ) {
return new OO.ui.Process( function () {
var error = dialog.validate();
if ( error ) {
return $.Deferred().reject( new OO.ui.Error( error, { recoverable: true } ) ).promise();
}
dialog.actions.setMode( 'uploading' );
dialog.$dropZone.hide();
dialog.optionsFieldset.$element.hide();
dialog.overallProgress.toggle( true );
dialog.rows.forEach( function ( row ) {
row.setBusy( true );
} );
return dialog.runUploads().then( function () {
return dialog.finish();
} );
} );
}
if ( action === 'abort' ) {
return new OO.ui.Process( function () {
dialog.aborted = true;
dialog.statusLabel.setLabel( 'Stopping…' );
if ( dialog.currentUpload && dialog.currentUpload.abort ) {
dialog.currentUpload.abort();
}
} );
}
if ( action === 'finish' ) {
return new OO.ui.Process( function () {
dialog.close( { action: 'finish' } );
} );
}
return BulkUploadDialog.super.prototype.getActionProcess.call( this, action );
};
BulkUploadDialog.prototype.validate = function () {
var seen = {},
problem = null;
if ( !this.rows.length ) {
return 'Add at least one file.';
}
this.rows.forEach( function ( row ) {
var name = row.getFileName();
if ( !name || name === '.' + getExtension( name ) ) {
problem = problem || 'Every file needs a name.';
return;
}
if ( !getExtension( name ) ) {
problem = problem || 'File names must keep their extension (' + name + ').';
return;
}
if ( seen[ name ] && row.getPolicy() !== 'skip' ) {
problem = problem || 'Two files are queued as "' + name + '". Rename one of them.';
}
seen[ name ] = true;
} );
return problem;
};
/** Upload every queued file, one at a time. Never rejects. */
BulkUploadDialog.prototype.runUploads = function () {
var dialog = this,
queue = this.rows.filter( function ( row ) {
return row.getPolicy() !== 'skip';
} ),
total = queue.length,
index = 0;
function next() {
var row;
if ( dialog.aborted || index >= total ) {
return $.Deferred().resolve().promise();
}
row = queue[ index ];
dialog.statusLabel.setLabel(
'Uploading ' + ( index + 1 ) + ' of ' + total + ': ' + row.getFileName()
);
return dialog.uploadRow( row, index, total ).then( function () {
index++;
dialog.overallProgress.setProgress( Math.round( ( index / total ) * 100 ) );
return next();
} );
}
return next();
};
/** Upload a single row. Always resolves; failures are recorded on the row. */
BulkUploadDialog.prototype.uploadRow = function ( row, index, total ) {
var dialog = this,
deferred = $.Deferred(),
name = row.getFileName(),
policy = row.getPolicy(),
useCaptions = this.captionsInput.isSelected(),
caption = useCaptions ? captionFor( row.file.name ) : '';
function record( finalName, note ) {
row.done = true;
row.uploadedName = finalName;
row.setProgress( 1 );
row.setStatus( note || 'Uploaded.', 'done' );
dialog.results.push( {
name: finalName,
caption: caption,
isImage: isImageName( finalName )
} );
deferred.resolve();
}
function fail( message ) {
row.setStatus( message, 'error' );
dialog.failures.push( { name: name, message: message } );
deferred.resolve();
}
if ( policy === 'existing' ) {
record( name, 'Used the file already on the wiki.' );
return deferred.promise();
}
if ( policy === 'rename' && row.exists ) {
name = this.uniqueName( name );
row.nameInput.setValue( name );
}
this.currentUpload = getApi().upload( row.file, {
filename: name,
comment: 'Bulk upload from the visual editor',
text: this.buildFilePageText( caption ),
ignorewarnings: policy === 'overwrite' ? 1 : undefined
} );
this.currentUpload.progress( function ( fraction ) {
row.setProgress( fraction );
dialog.overallProgress.setProgress(
Math.round( ( ( index + fraction ) / total ) * 100 )
);
} ).done( function ( result ) {
var uploaded = result && result.upload;
record( ( uploaded && uploaded.filename ) || name );
} ).fail( function ( code, result ) {
var warnings = result && result.upload && result.upload.warnings;
if ( dialog.aborted ) {
row.setStatus( 'Cancelled.', 'warning' );
deferred.resolve();
return;
}
if ( warnings && warnings.duplicate && warnings.duplicate.length ) {
// Byte-identical file already on the wiki — use it instead of
// storing a second copy.
record( warnings.duplicate[ 0 ], 'Identical file already on the wiki — used that one.' );
return;
}
if ( warnings && ( warnings.exists || warnings[ 'exists-normalized' ] ) ) {
fail( 'A file called "' + name + '" already exists. Choose a different name, or "Overwrite".' );
return;
}
if ( warnings && warnings[ 'was-deleted' ] ) {
fail( 'A file with this name was deleted before. Rename it, or upload it via Special:Upload.' );
return;
}
if ( warnings ) {
fail( 'Upload warning: ' + Object.keys( warnings ).join( ', ' ) );
return;
}
fail( apiErrorMessage( code, result ) );
} ).always( function () {
dialog.currentUpload = null;
} );
return deferred.promise();
};
/** Find a free "Name-1.jpg" style variant. */
BulkUploadDialog.prototype.uniqueName = function ( name ) {
var base = stripExtension( name ),
ext = getExtension( name ),
taken = {},
candidate,
i = 1;
this.rows.forEach( function ( row ) {
if ( row.uploadedName ) {
taken[ row.uploadedName ] = true;
}
} );
this.results.forEach( function ( result ) {
taken[ result.name ] = true;
} );
do {
candidate = base + '-' + i + '.' + ext;
i++;
} while ( taken[ candidate ] && i < 1000 );
return candidate;
};
BulkUploadDialog.prototype.buildFilePageText = function ( caption ) {
var parts = [],
description = this.descriptionInput.getValue().trim(),
categories = this.categoriesInput.getValue();
if ( description ) {
parts.push( description );
} else if ( caption ) {
parts.push( caption );
}
if ( categories.length ) {
parts.push( categories.map( function ( category ) {
return '[[Category:' + category + ']]';
} ).join( '\n' ) );
}
return parts.join( '\n\n' );
};
/** Build the wikitext for everything that uploaded successfully. */
BulkUploadDialog.prototype.buildWikitext = function () {
var insertAs = this.insertAsInput.getValue(),
heights = parseInt( this.heightsInput.getValue(), 10 ) || 180,
images = this.results.filter( function ( r ) {
return r.isImage;
} ),
others = this.results.filter( function ( r ) {
return !r.isImage;
} ),
parts = [];
function link( result ) {
return '* [[Media:' + result.name + '|' + ( result.caption || result.name ) + ']]';
}
if ( images.length ) {
if ( insertAs === 'gallery' ) {
parts.push(
'<gallery mode="packed" heights="' + heights + '">\n' +
images.map( function ( result ) {
return 'File:' + result.name + ( result.caption ? '|' + result.caption : '' );
} ).join( '\n' ) +
'\n</gallery>'
);
} else if ( insertAs === 'thumbs' ) {
parts.push( images.map( function ( result ) {
return '[[File:' + result.name + '|thumb|' + ( result.caption || '' ) + ']]';
} ).join( '\n' ) );
} else {
parts.push( images.map( link ).join( '\n' ) );
}
}
if ( others.length ) {
parts.push( others.map( link ).join( '\n' ) );
}
return parts.join( '\n\n' );
};
/**
* Insert wikitext at the target fragment, reusing VisualEditor's own
* wikitext → document conversion (same path as pasting wikitext).
*
* @param {string} wikitext
* @return {jQuery.Promise}
*/
BulkUploadDialog.prototype.insertWikitext = function ( wikitext ) {
var fragment = this.getFragment(),
surface = ( this.manager && this.manager.getSurface && this.manager.getSurface() ) ||
( ve.init.target && ve.init.target.getSurface() ),
item;
if ( !surface || !fragment ) {
return $.Deferred().reject( 'no surface' ).promise();
}
item = ve.ui.DataTransferItem.static.newFromString( wikitext, 'text/plain' );
return ve.ui.dataTransferHandlerFactory.create( 'wikitextString', surface, item )
.getInsertableData()
.then( function ( docOrData ) {
var target = fragment.collapseToEnd();
if ( docOrData instanceof ve.dm.Document ) {
target.insertDocument( docOrData );
} else {
target.insertContent( docOrData );
}
target.collapseToEnd().select();
} );
};
/** Insert what succeeded, then close or report. */
BulkUploadDialog.prototype.finish = function () {
var dialog = this,
wikitext;
this.overallProgress.setProgress( 100 );
if ( this.aborted ) {
this.statusLabel.setLabel( 'Upload stopped — nothing was inserted.' );
this.showResults( true );
return $.Deferred().resolve().promise();
}
if ( !this.results.length ) {
this.statusLabel.setLabel( 'Nothing was uploaded.' );
this.showResults( false );
return $.Deferred().resolve().promise();
}
this.statusLabel.setLabel( 'Inserting…' );
wikitext = this.buildWikitext();
return this.insertWikitext( wikitext ).then( function () {
if ( dialog.failures.length ) {
dialog.statusLabel.setLabel( 'Inserted, but some files failed.' );
dialog.showResults( false );
return;
}
dialog.close( { action: 'inserted' } );
}, function () {
// Insertion failed — the files are uploaded, so hand over the wikitext.
dialog.statusLabel.setLabel( 'The files uploaded, but could not be inserted automatically.' );
dialog.showResults( false, wikitext );
} );
};
BulkUploadDialog.prototype.showResults = function ( aborted, wikitext ) {
var $list = $( '<ul>' );
this.actions.setMode( 'result' );
this.$results.empty().show();
if ( this.results.length ) {
this.$results.append( $( '<p>' ).text(
aborted ?
'Uploaded before stopping (not inserted — you can find them under Special:NewFiles):' :
'Uploaded:'
) );
this.results.forEach( function ( result ) {
$list.append( $( '<li>' ).append(
$( '<a>' )
.attr( { href: mw.util.getUrl( 'File:' + result.name ), target: '_blank' } )
.text( result.name )
) );
} );
this.$results.append( $list );
}
if ( this.failures.length ) {
this.$results.append( $( '<p>' ).addClass( 've-bulkupload-status-error' ).text( 'Failed:' ) );
this.$results.append( $( '<ul>' ).append( this.failures.map( function ( failure ) {
return $( '<li>' ).text( failure.name + ' — ' + failure.message );
} ) ) );
}
if ( wikitext ) {
this.$results.append(
$( '<p>' ).text( 'Copy this into the page yourself:' ),
$( '<pre>' ).text( wikitext )
);
}
this.updateSize();
};
ve.ui.windowFactory.register( BulkUploadDialog );
/* --------------------------------------------------------- command/tool -- */
ve.ui.commandRegistry.register(
new ve.ui.Command( 'bulkUpload', 'window', 'open', { args: [ 'veBulkUpload' ] } )
);
/**
* @class
*/
function BulkUploadTool() {
BulkUploadTool.super.apply( this, arguments );
}
OO.inheritClass( BulkUploadTool, ve.ui.FragmentWindowTool );
BulkUploadTool.static.name = 'bulkUpload';
BulkUploadTool.static.group = 'object';
BulkUploadTool.static.icon = 'imageGallery';
BulkUploadTool.static.title = 'Upload files';
BulkUploadTool.static.commandName = 'bulkUpload';
ve.ui.toolFactory.register( BulkUploadTool );
/* ----------------------------------------------------------- drop hook -- */
var $toast = null,
toastTimeout = null;
function showToast( text ) {
if ( !$toast ) {
$toast = $( '<div>' ).addClass( 've-bulkupload-toast' ).appendTo( document.body );
}
$toast.text( text ).addClass( 've-bulkupload-toast-visible' );
if ( toastTimeout ) {
clearTimeout( toastTimeout );
}
toastTimeout = setTimeout( hideToast, 2500 );
}
function hideToast() {
if ( $toast ) {
$toast.removeClass( 've-bulkupload-toast-visible' );
}
}
function countDraggedFiles( dataTransfer ) {
if ( !dataTransfer ) {
return 0;
}
if ( dataTransfer.items && dataTransfer.items.length ) {
return Array.prototype.filter.call( dataTransfer.items, function ( item ) {
return item.kind === 'file';
} ).length;
}
return dataTransfer.files ? dataTransfer.files.length : 0;
}
function attachDropHandlers( surface ) {
var view = surface.getView(),
element = view.$element[ 0 ];
if ( !element || element.veBulkUploadAttached ) {
return;
}
element.veBulkUploadAttached = true;
element.addEventListener( 'dragover', function ( e ) {
var count = countDraggedFiles( e.dataTransfer );
if ( count > 1 ) {
showToast( 'Drop ' + count + ' files to upload them all' );
}
}, true );
element.addEventListener( 'dragleave', hideToast, true );
element.addEventListener( 'drop', function ( e ) {
var files = e.dataTransfer && e.dataTransfer.files,
model = surface.getModel(),
offset,
fragment;
hideToast();
// One file: leave it to VisualEditor's own media dialog.
if ( !files || files.length < 2 || surface.getMode() !== 'visual' ) {
return;
}
e.preventDefault();
e.stopPropagation();
offset = view.getOffsetFromEventCoords( e );
if ( offset !== -1 && offset !== undefined ) {
model.setLinearSelection( new ve.Range( offset ) );
}
fragment = model.getFragment().collapseToEnd();
surface.execute( 'window', 'open', 'veBulkUpload', {
files: Array.prototype.slice.call( files ),
fragment: fragment
} );
}, true );
}
mw.hook( 've.activationComplete' ).add( function () {
var surface = ve.init.target && ve.init.target.getSurface();
if ( surface ) {
attachDropHandlers( surface );
}
} );
}() );