Merge "Fix spelling mistakes in comments"
[mediawiki.git] / resources / src / mediawiki / mediawiki.Upload.BookletLayout.js
blob43c3c8eb8932520800050efbbb92f2434898d369
1 ( function ( $, mw ) {
3         /**
4          * mw.Upload.BookletLayout encapsulates the process of uploading a file
5          * to MediaWiki using the {@link mw.Upload upload model}.
6          * The booklet emits events that can be used to get the stashed
7          * upload and the final file. It can be extended to accept
8          * additional fields from the user for specific scenarios like
9          * for Commons, or campaigns.
10          *
11          * ## Structure
12          *
13          * The {@link OO.ui.BookletLayout booklet layout} has three steps:
14          *
15          *  - **Upload**: Has a {@link OO.ui.SelectFileWidget field} to get the file object.
16          *
17          * - **Information**: Has a {@link OO.ui.FormLayout form} to collect metadata. This can be
18          *   extended.
19          *
20          * - **Insert**: Has details on how to use the file that was uploaded.
21          *
22          * Each step has a form associated with it defined in
23          * {@link #renderUploadForm renderUploadForm},
24          * {@link #renderInfoForm renderInfoForm}, and
25          * {@link #renderInsertForm renderInfoForm}. The
26          * {@link #getFile getFile},
27          * {@link #getFilename getFilename}, and
28          * {@link #getText getText} methods are used to get
29          * the information filled in these forms, required to call
30          * {@link mw.Upload mw.Upload}.
31          *
32          * ## Usage
33          *
34          * See the {@link mw.Upload.Dialog upload dialog}.
35          *
36          * The {@link #event-fileUploaded fileUploaded},
37          * and {@link #event-fileSaved fileSaved} events can
38          * be used to get details of the upload.
39          *
40          * ## Extending
41          *
42          * To extend using {@link mw.Upload mw.Upload}, override
43          * {@link #renderInfoForm renderInfoForm} to render
44          * the form required for the specific use-case. Update the
45          * {@link #getFilename getFilename}, and
46          * {@link #getText getText} methods to return data
47          * from your newly created form. If you added new fields you'll also have
48          * to update the {@link #clear} method.
49          *
50          * If you plan to use a different upload model, apart from what is mentioned
51          * above, you'll also have to override the
52          * {@link #createUpload createUpload} method to
53          * return the new model. The {@link #saveFile saveFile}, and
54          * the {@link #uploadFile uploadFile} methods need to be
55          * overridden to use the new model and data returned from the forms.
56          *
57          * @class
58          * @extends OO.ui.BookletLayout
59          *
60          * @constructor
61          * @param {Object} config Configuration options
62          * @cfg {jQuery} [$overlay] Overlay to use for widgets in the booklet
63          */
64         mw.Upload.BookletLayout = function ( config ) {
65                 // Parent constructor
66                 mw.Upload.BookletLayout.parent.call( this, config );
68                 this.$overlay = config.$overlay;
70                 this.renderUploadForm();
71                 this.renderInfoForm();
72                 this.renderInsertForm();
74                 this.addPages( [
75                         new OO.ui.PageLayout( 'upload', {
76                                 scrollable: true,
77                                 padded: true,
78                                 content: [ this.uploadForm ]
79                         } ),
80                         new OO.ui.PageLayout( 'info', {
81                                 scrollable: true,
82                                 padded: true,
83                                 content: [ this.infoForm ]
84                         } ),
85                         new OO.ui.PageLayout( 'insert', {
86                                 scrollable: true,
87                                 padded: true,
88                                 content: [ this.insertForm ]
89                         } )
90                 ] );
91         };
93         /* Setup */
95         OO.inheritClass( mw.Upload.BookletLayout, OO.ui.BookletLayout );
97         /* Events */
99         /**
100          * The file has finished uploading
101          *
102          * @event fileUploaded
103          */
105         /**
106          * The file has been saved to the database
107          *
108          * @event fileSaved
109          * @param {Object} imageInfo See mw.Upload#getImageInfo
110          */
112         /**
113          * The upload form has changed
114          *
115          * @event uploadValid
116          * @param {boolean} isValid The form is valid
117          */
119         /**
120          * The info form has changed
121          *
122          * @event infoValid
123          * @param {boolean} isValid The form is valid
124          */
126         /* Properties */
128         /**
129          * @property {OO.ui.FormLayout} uploadForm
130          * The form rendered in the first step to get the file object.
131          * Rendered in {@link #renderUploadForm renderUploadForm}.
132          */
134         /**
135          * @property {OO.ui.FormLayout} infoForm
136          * The form rendered in the second step to get metadata.
137          * Rendered in {@link #renderInfoForm renderInfoForm}
138          */
140         /**
141          * @property {OO.ui.FormLayout} insertForm
142          * The form rendered in the third step to show usage
143          * Rendered in {@link #renderInsertForm renderInsertForm}
144          */
146         /* Methods */
148         /**
149          * Initialize for a new upload
150          *
151          * @return {jQuery.Promise} Promise resolved when everything is initialized
152          */
153         mw.Upload.BookletLayout.prototype.initialize = function () {
154                 var
155                         apiPromise,
156                         booklet = this,
157                         deferred = $.Deferred();
159                 this.clear();
160                 this.upload = this.createUpload();
161                 this.setPage( 'upload' );
163                 apiPromise = this.upload.apiPromise || $.Deferred.resolve( this.upload.api );
164                 apiPromise.done( function ( api ) {
165                         // If the user can't upload anything, don't give them the option to.
166                         api.getUserInfo().done( function ( userInfo ) {
167                                 if ( userInfo.rights.indexOf( 'upload' ) === -1 ) {
168                                         // TODO Use a better error message when not all logged-in users can upload
169                                         booklet.getPage( 'upload' ).$element.msg( 'api-error-mustbeloggedin' );
170                                 }
171                         } ).always( function () {
172                                 deferred.resolve();
173                         } );
174                 } );
176                 return deferred.promise();
177         };
179         /**
180          * Create a new upload model
181          *
182          * @protected
183          * @return {mw.Upload} Upload model
184          */
185         mw.Upload.BookletLayout.prototype.createUpload = function () {
186                 return new mw.Upload();
187         };
189         /* Uploading */
191         /**
192          * Uploads the file that was added in the upload form. Uses
193          * {@link #getFile getFile} to get the HTML5
194          * file object.
195          *
196          * @protected
197          * @fires fileUploaded
198          * @return {jQuery.Promise}
199          */
200         mw.Upload.BookletLayout.prototype.uploadFile = function () {
201                 var deferred = $.Deferred(),
202                         layout = this,
203                         file = this.getFile();
205                 this.filenameWidget.setValue( file.name );
206                 this.setPage( 'info' );
208                 if ( this.shouldRecordBucket ) {
209                         this.upload.bucket = this.bucket;
210                 }
212                 this.upload.setFile( file );
213                 // Explicitly set the filename so that the old filename isn't used in case of retry
214                 this.upload.setFilenameFromFile();
216                 this.uploadPromise = this.upload.uploadToStash();
217                 this.uploadPromise.then( function () {
218                         deferred.resolve();
219                         layout.emit( 'fileUploaded' );
220                 }, function () {
221                         // These errors will be thrown while the user is on the info page.
222                         // Pretty sure it's impossible to get a warning other than 'stashfailed' here, which should
223                         // really be an error...
224                         var errorMessage = layout.getErrorMessageForStateDetails();
225                         deferred.reject( errorMessage );
226                 } );
228                 // If there is an error in uploading, come back to the upload page
229                 deferred.fail( function () {
230                         layout.setPage( 'upload' );
231                 } );
233                 return deferred;
234         };
236         /**
237          * Saves the stash finalizes upload. Uses
238          * {@link #getFilename getFilename}, and
239          * {@link #getText getText} to get details from
240          * the form.
241          *
242          * @protected
243          * @fires fileSaved
244          * @return {jQuery.Promise} Rejects the promise with an
245          * {@link OO.ui.Error error}, or resolves if the upload was successful.
246          */
247         mw.Upload.BookletLayout.prototype.saveFile = function () {
248                 var layout = this,
249                         deferred = $.Deferred();
251                 this.upload.setFilename( this.getFilename() );
252                 this.upload.setText( this.getText() );
254                 this.uploadPromise.then( function () {
255                         layout.upload.finishStashUpload().then( function () {
256                                 var name;
258                                 // Normalize page name and localise the 'File:' prefix
259                                 name = new mw.Title( 'File:' + layout.upload.getFilename() ).toString();
260                                 layout.filenameUsageWidget.setValue( '[[' + name + ']]' );
261                                 layout.setPage( 'insert' );
263                                 deferred.resolve();
264                                 layout.emit( 'fileSaved', layout.upload.getImageInfo() );
265                         }, function () {
266                                 var errorMessage = layout.getErrorMessageForStateDetails();
267                                 deferred.reject( errorMessage );
268                         } );
269                 } );
271                 return deferred.promise();
272         };
274         /**
275          * Get an error message (as OO.ui.Error object) that should be displayed to the user for current
276          * state and state details.
277          *
278          * @protected
279          * @return {OO.ui.Error} Error to display for given state and details.
280          */
281         mw.Upload.BookletLayout.prototype.getErrorMessageForStateDetails = function () {
282                 var message,
283                         state = this.upload.getState(),
284                         stateDetails = this.upload.getStateDetails(),
285                         error = stateDetails.error,
286                         warnings = stateDetails.upload && stateDetails.upload.warnings;
288                 if ( state === mw.Upload.State.ERROR ) {
289                         // HACK We should either have a hook here to allow TitleBlacklist to handle this, or just have
290                         // TitleBlacklist produce sane error messages that can be displayed without arcane knowledge
291                         if ( error.info === 'TitleBlacklist prevents this title from being created' ) {
292                                 // HACK Apparently the only reliable way to determine whether TitleBlacklist was involved
293                                 return new OO.ui.Error(
294                                         $( '<p>' ).html(
295                                                 // HACK TitleBlacklist doesn't have a sensible message, this one is from UploadWizard
296                                                 mw.message( 'api-error-blacklisted' ).parse()
297                                         ),
298                                         { recoverable: false }
299                                 );
300                         }
302                         message = mw.message( 'api-error-' + error.code );
303                         if ( !message.exists() ) {
304                                 message = mw.message( 'api-error-unknownerror', JSON.stringify( stateDetails ) );
305                         }
306                         return new OO.ui.Error(
307                                 $( '<p>' ).html(
308                                         message.parse()
309                                 ),
310                                 { recoverable: false }
311                         );
312                 }
314                 if ( state === mw.Upload.State.WARNING ) {
315                         // We could get more than one of these errors, these are in order
316                         // of importance. For example fixing the thumbnail like file name
317                         // won't help the fact that the file already exists.
318                         if ( warnings.stashfailed !== undefined ) {
319                                 return new OO.ui.Error(
320                                         $( '<p>' ).msg( 'api-error-stashfailed' ),
321                                         { recoverable: false }
322                                 );
323                         } else if ( warnings.exists !== undefined ) {
324                                 return new OO.ui.Error(
325                                         $( '<p>' ).msg( 'fileexists', 'File:' + warnings.exists ),
326                                         { recoverable: false }
327                                 );
328                         } else if ( warnings[ 'page-exists' ] !== undefined ) {
329                                 return new OO.ui.Error(
330                                         $( '<p>' ).msg( 'filepageexists', 'File:' + warnings[ 'page-exists' ] ),
331                                         { recoverable: false }
332                                 );
333                         } else if ( warnings.duplicate !== undefined ) {
334                                 return new OO.ui.Error(
335                                         $( '<p>' ).msg( 'api-error-duplicate', warnings.duplicate.length ),
336                                         { recoverable: false }
337                                 );
338                         } else if ( warnings[ 'thumb-name' ] !== undefined ) {
339                                 return new OO.ui.Error(
340                                         $( '<p>' ).msg( 'filename-thumb-name' ),
341                                         { recoverable: false }
342                                 );
343                         } else if ( warnings[ 'bad-prefix' ] !== undefined ) {
344                                 return new OO.ui.Error(
345                                         $( '<p>' ).msg( 'filename-bad-prefix', warnings[ 'bad-prefix' ] ),
346                                         { recoverable: false }
347                                 );
348                         } else if ( warnings[ 'duplicate-archive' ] !== undefined ) {
349                                 return new OO.ui.Error(
350                                         $( '<p>' ).msg( 'api-error-duplicate-archive', 1 ),
351                                         { recoverable: false }
352                                 );
353                         } else if ( warnings.badfilename !== undefined ) {
354                                 // Change the name if the current name isn't acceptable
355                                 // TODO This might not really be the best place to do this
356                                 this.filenameWidget.setValue( warnings.badfilename );
357                                 return new OO.ui.Error(
358                                         $( '<p>' ).msg( 'badfilename', warnings.badfilename )
359                                 );
360                         } else {
361                                 return new OO.ui.Error(
362                                         $( '<p>' ).html(
363                                                 // Let's get all the help we can if we can't pin point the error
364                                                 mw.message( 'api-error-unknown-warning', JSON.stringify( stateDetails ) ).parse()
365                                         ),
366                                         { recoverable: false }
367                                 );
368                         }
369                 }
370         };
372         /* Form renderers */
374         /**
375          * Renders and returns the upload form and sets the
376          * {@link #uploadForm uploadForm} property.
377          *
378          * @protected
379          * @fires selectFile
380          * @return {OO.ui.FormLayout}
381          */
382         mw.Upload.BookletLayout.prototype.renderUploadForm = function () {
383                 var fieldset;
385                 this.selectFileWidget = new OO.ui.SelectFileWidget();
386                 fieldset = new OO.ui.FieldsetLayout( { label: mw.msg( 'upload-form-label-select-file' ) } );
387                 fieldset.addItems( [ this.selectFileWidget ] );
388                 this.uploadForm = new OO.ui.FormLayout( { items: [ fieldset ] } );
390                 // Validation
391                 this.selectFileWidget.on( 'change', this.onUploadFormChange.bind( this ) );
393                 return this.uploadForm;
394         };
396         /**
397          * Handle change events to the upload form
398          *
399          * @protected
400          * @fires uploadValid
401          */
402         mw.Upload.BookletLayout.prototype.onUploadFormChange = function () {
403                 this.emit( 'uploadValid', !!this.selectFileWidget.getValue() );
404         };
406         /**
407          * Renders and returns the information form for collecting
408          * metadata and sets the {@link #infoForm infoForm}
409          * property.
410          *
411          * @protected
412          * @return {OO.ui.FormLayout}
413          */
414         mw.Upload.BookletLayout.prototype.renderInfoForm = function () {
415                 var fieldset;
417                 this.filenameWidget = new OO.ui.TextInputWidget( {
418                         indicator: 'required',
419                         required: true,
420                         validate: /.+/
421                 } );
422                 this.descriptionWidget = new OO.ui.TextInputWidget( {
423                         indicator: 'required',
424                         required: true,
425                         validate: /.+/,
426                         multiline: true,
427                         autosize: true
428                 } );
430                 fieldset = new OO.ui.FieldsetLayout( {
431                         label: mw.msg( 'upload-form-label-infoform-title' )
432                 } );
433                 fieldset.addItems( [
434                         new OO.ui.FieldLayout( this.filenameWidget, {
435                                 label: mw.msg( 'upload-form-label-infoform-name' ),
436                                 align: 'top'
437                         } ),
438                         new OO.ui.FieldLayout( this.descriptionWidget, {
439                                 label: mw.msg( 'upload-form-label-infoform-description' ),
440                                 align: 'top'
441                         } )
442                 ] );
443                 this.infoForm = new OO.ui.FormLayout( { items: [ fieldset ] } );
445                 this.filenameWidget.on( 'change', this.onInfoFormChange.bind( this ) );
446                 this.descriptionWidget.on( 'change', this.onInfoFormChange.bind( this ) );
448                 return this.infoForm;
449         };
451         /**
452          * Handle change events to the info form
453          *
454          * @protected
455          * @fires infoValid
456          */
457         mw.Upload.BookletLayout.prototype.onInfoFormChange = function () {
458                 var layout = this;
459                 $.when(
460                         this.filenameWidget.getValidity(),
461                         this.descriptionWidget.getValidity()
462                 ).done( function () {
463                         layout.emit( 'infoValid', true );
464                 } ).fail( function () {
465                         layout.emit( 'infoValid', false );
466                 } );
467         };
469         /**
470          * Renders and returns the insert form to show file usage and
471          * sets the {@link #insertForm insertForm} property.
472          *
473          * @protected
474          * @return {OO.ui.FormLayout}
475          */
476         mw.Upload.BookletLayout.prototype.renderInsertForm = function () {
477                 var fieldset;
479                 this.filenameUsageWidget = new OO.ui.TextInputWidget();
480                 fieldset = new OO.ui.FieldsetLayout( {
481                         label: mw.msg( 'upload-form-label-usage-title' )
482                 } );
483                 fieldset.addItems( [
484                         new OO.ui.FieldLayout( this.filenameUsageWidget, {
485                                 label: mw.msg( 'upload-form-label-usage-filename' ),
486                                 align: 'top'
487                         } )
488                 ] );
489                 this.insertForm = new OO.ui.FormLayout( { items: [ fieldset ] } );
491                 return this.insertForm;
492         };
494         /* Getters */
496         /**
497          * Gets the file object from the
498          * {@link #uploadForm upload form}.
499          *
500          * @protected
501          * @return {File|null}
502          */
503         mw.Upload.BookletLayout.prototype.getFile = function () {
504                 return this.selectFileWidget.getValue();
505         };
507         /**
508          * Gets the file name from the
509          * {@link #infoForm information form}.
510          *
511          * @protected
512          * @return {string}
513          */
514         mw.Upload.BookletLayout.prototype.getFilename = function () {
515                 return this.filenameWidget.getValue();
516         };
518         /**
519          * Gets the page text from the
520          * {@link #infoForm information form}.
521          *
522          * @protected
523          * @return {string}
524          */
525         mw.Upload.BookletLayout.prototype.getText = function () {
526                 return this.descriptionWidget.getValue();
527         };
529         /* Setters */
531         /**
532          * Sets the file object
533          *
534          * @protected
535          * @param {File|null} file File to select
536          */
537         mw.Upload.BookletLayout.prototype.setFile = function ( file ) {
538                 this.selectFileWidget.setValue( file );
539         };
541         /**
542          * Clear the values of all fields
543          *
544          * @protected
545          */
546         mw.Upload.BookletLayout.prototype.clear = function () {
547                 this.selectFileWidget.setValue( null );
548                 this.filenameWidget.setValue( null ).setValidityFlag( true );
549                 this.descriptionWidget.setValue( null ).setValidityFlag( true );
550                 this.filenameUsageWidget.setValue( null );
551         };
553 }( jQuery, mediaWiki ) );