/* global moment */ ( function ( $, mw, moment ) { /** * mw.Upload.BookletLayout encapsulates the process of uploading a file * to MediaWiki using the {@link mw.Upload upload model}. * The booklet emits events that can be used to get the stashed * upload and the final file. It can be extended to accept * additional fields from the user for specific scenarios like * for Commons, or campaigns. * * ## Structure * * The {@link OO.ui.BookletLayout booklet layout} has three steps: * * - **Upload**: Has a {@link OO.ui.SelectFileWidget field} to get the file object. * * - **Information**: Has a {@link OO.ui.FormLayout form} to collect metadata. This can be * extended. * * - **Insert**: Has details on how to use the file that was uploaded. * * Each step has a form associated with it defined in * {@link #renderUploadForm renderUploadForm}, * {@link #renderInfoForm renderInfoForm}, and * {@link #renderInsertForm renderInfoForm}. The * {@link #getFile getFile}, * {@link #getFilename getFilename}, and * {@link #getText getText} methods are used to get * the information filled in these forms, required to call * {@link mw.Upload mw.Upload}. * * ## Usage * * See the {@link mw.Upload.Dialog upload dialog}. * * The {@link #event-fileUploaded fileUploaded}, * and {@link #event-fileSaved fileSaved} events can * be used to get details of the upload. * * ## Extending * * To extend using {@link mw.Upload mw.Upload}, override * {@link #renderInfoForm renderInfoForm} to render * the form required for the specific use-case. Update the * {@link #getFilename getFilename}, and * {@link #getText getText} methods to return data * from your newly created form. If you added new fields you'll also have * to update the {@link #clear} method. * * If you plan to use a different upload model, apart from what is mentioned * above, you'll also have to override the * {@link #createUpload createUpload} method to * return the new model. The {@link #saveFile saveFile}, and * the {@link #uploadFile uploadFile} methods need to be * overridden to use the new model and data returned from the forms. * * @class * @extends OO.ui.BookletLayout * * @constructor * @param {Object} config Configuration options * @cfg {jQuery} [$overlay] Overlay to use for widgets in the booklet * @cfg {string} [filekey] Sets the stashed file to finish uploading. Overrides most of the file selection process, and fetches a thumbnail from the server. */ mw.Upload.BookletLayout = function ( config ) { // Parent constructor mw.Upload.BookletLayout.parent.call( this, config ); this.$overlay = config.$overlay; this.filekey = config.filekey; this.renderUploadForm(); this.renderInfoForm(); this.renderInsertForm(); this.addPages( [ new OO.ui.PageLayout( 'upload', { scrollable: true, padded: true, content: [ this.uploadForm ] } ), new OO.ui.PageLayout( 'info', { scrollable: true, padded: true, content: [ this.infoForm ] } ), new OO.ui.PageLayout( 'insert', { scrollable: true, padded: true, content: [ this.insertForm ] } ) ] ); }; /* Setup */ OO.inheritClass( mw.Upload.BookletLayout, OO.ui.BookletLayout ); /* Events */ /** * Progress events for the uploaded file * * @event fileUploadProgress * @param {number} progress In percentage * @param {Object} duration Duration object from `moment.duration()` */ /** * The file has finished uploading * * @event fileUploaded */ /** * The file has been saved to the database * * @event fileSaved * @param {Object} imageInfo See mw.Upload#getImageInfo */ /** * The upload form has changed * * @event uploadValid * @param {boolean} isValid The form is valid */ /** * The info form has changed * * @event infoValid * @param {boolean} isValid The form is valid */ /* Properties */ /** * @property {OO.ui.FormLayout} uploadForm * The form rendered in the first step to get the file object. * Rendered in {@link #renderUploadForm renderUploadForm}. */ /** * @property {OO.ui.FormLayout} infoForm * The form rendered in the second step to get metadata. * Rendered in {@link #renderInfoForm renderInfoForm} */ /** * @property {OO.ui.FormLayout} insertForm * The form rendered in the third step to show usage * Rendered in {@link #renderInsertForm renderInsertForm} */ /* Methods */ /** * Initialize for a new upload * * @return {jQuery.Promise} Promise resolved when everything is initialized */ mw.Upload.BookletLayout.prototype.initialize = function () { var booklet = this; this.clear(); this.upload = this.createUpload(); this.setPage( 'upload' ); if ( this.filekey ) { this.setFilekey( this.filekey ); } return this.upload.getApi().then( function ( api ) { // If the user can't upload anything, don't give them the option to. return api.getUserInfo().then( function ( userInfo ) { if ( userInfo.rights.indexOf( 'upload' ) === -1 ) { if ( mw.user.isAnon() ) { booklet.getPage( 'upload' ).$element.msg( 'apierror-mustbeloggedin', mw.msg( 'action-upload' ) ); } else { booklet.getPage( 'upload' ).$element.msg( 'apierror-permissiondenied', mw.msg( 'action-upload' ) ); } } return $.Deferred().resolve(); }, // Always resolve, never reject function () { return $.Deferred().resolve(); } ); }, function ( errorMsg ) { booklet.getPage( 'upload' ).$element.msg( errorMsg ); return $.Deferred().resolve(); } ); }; /** * Create a new upload model * * @protected * @return {mw.Upload} Upload model */ mw.Upload.BookletLayout.prototype.createUpload = function () { return new mw.Upload( { parameters: { errorformat: 'html', errorlang: mw.config.get( 'wgUserLanguage' ), errorsuselocal: 1, formatversion: 2 } } ); }; /* Uploading */ /** * Uploads the file that was added in the upload form. Uses * {@link #getFile getFile} to get the HTML5 * file object. * * @protected * @fires fileUploadProgress * @fires fileUploaded * @return {jQuery.Promise} */ mw.Upload.BookletLayout.prototype.uploadFile = function () { var deferred = $.Deferred(), startTime = mw.now(), layout = this, file = this.getFile(); this.setPage( 'info' ); if ( this.filekey ) { if ( file === null ) { // Someone gonna get-a hurt real bad throw new Error( 'filekey not passed into file select widget, which is impossible. Quitting while we\'re behind.' ); } // Stashed file already uploaded. deferred.resolve(); this.uploadPromise = deferred; this.emit( 'fileUploaded' ); return deferred; } this.setFilename( file.name ); this.upload.setFile( file ); // The original file name might contain invalid characters, so use our sanitized one this.upload.setFilename( this.getFilename() ); this.uploadPromise = this.upload.uploadToStash(); this.uploadPromise.then( function () { deferred.resolve(); layout.emit( 'fileUploaded' ); }, function () { // These errors will be thrown while the user is on the info page. layout.getErrorMessageForStateDetails().then( function ( errorMessage ) { deferred.reject( errorMessage ); } ); }, function ( progress ) { var elapsedTime = mw.now() - startTime, estimatedTotalTime = ( 1 / progress ) * elapsedTime, estimatedRemainingTime = moment.duration( estimatedTotalTime - elapsedTime ); layout.emit( 'fileUploadProgress', progress, estimatedRemainingTime ); } ); // If there is an error in uploading, come back to the upload page deferred.fail( function () { layout.setPage( 'upload' ); } ); return deferred; }; /** * Saves the stash finalizes upload. Uses * {@link #getFilename getFilename}, and * {@link #getText getText} to get details from * the form. * * @protected * @fires fileSaved * @return {jQuery.Promise} Rejects the promise with an * {@link OO.ui.Error error}, or resolves if the upload was successful. */ mw.Upload.BookletLayout.prototype.saveFile = function () { var layout = this, deferred = $.Deferred(); this.upload.setFilename( this.getFilename() ); this.upload.setText( this.getText() ); this.uploadPromise.then( function () { layout.upload.finishStashUpload().then( function () { var name; // Normalize page name and localise the 'File:' prefix name = new mw.Title( 'File:' + layout.upload.getFilename() ).toString(); layout.filenameUsageWidget.setValue( '[[' + name + ']]' ); layout.setPage( 'insert' ); deferred.resolve(); layout.emit( 'fileSaved', layout.upload.getImageInfo() ); }, function () { layout.getErrorMessageForStateDetails().then( function ( errorMessage ) { deferred.reject( errorMessage ); } ); } ); } ); return deferred.promise(); }; /** * Get an error message (as OO.ui.Error object) that should be displayed to the user for current * state and state details. * * @protected * @return {jQuery.Promise} A Promise that will be resolved with an OO.ui.Error. */ mw.Upload.BookletLayout.prototype.getErrorMessageForStateDetails = function () { var state = this.upload.getState(), stateDetails = this.upload.getStateDetails(), error = stateDetails.errors ? stateDetails.errors[ 0 ] : false, warnings = stateDetails.upload && stateDetails.upload.warnings, $ul = $( '