Merge "rdbms: make transaction rounds apply DBO_TRX to DB_REPLICA connections"
[mediawiki.git] / resources / src / mediawiki.Upload.BookletLayout / BookletLayout.js
blobd4acf316a3e7ff7aeafdb61d75c3dab8be15024d
1 /* global moment */
2 ( function () {
4         /**
5          * @classdesc Encapsulates the process of uploading a file
6          * to MediaWiki using the {@link mw.Upload upload model}.
7          * The booklet emits events that can be used to get the stashed
8          * upload and the final file. It can be extended to accept
9          * additional fields from the user for specific scenarios like
10          * for Commons, or campaigns.
11          *
12          * ## Structure
13          *
14          * The {@link OO.ui.BookletLayout booklet layout} has three steps:
15          *
16          *  - **Upload**: Has a {@link OO.ui.SelectFileInputWidget field} to get the file object.
17          *
18          * - **Information**: Has a {@link OO.ui.FormLayout form} to collect metadata. This can be
19          *   extended.
20          *
21          * - **Insert**: Has details on how to use the file that was uploaded.
22          *
23          * Each step has a form associated with it defined in
24          * {@link mw.Upload.BookletLayout#renderUploadForm renderUploadForm},
25          * {@link mw.Upload.BookletLayout#renderInfoForm renderInfoForm}, and
26          * {@link mw.Upload.BookletLayout#renderInsertForm renderInfoForm}. The
27          * {@link mw.Upload.BookletLayout#getFile getFile},
28          * {@link mw.Upload.BookletLayout#getFilename getFilename}, and
29          * {@link mw.Upload.BookletLayout#getText getText} methods are used to get
30          * the information filled in these forms, required to call
31          * {@link mw.Upload mw.Upload}.
32          *
33          * ## Usage
34          *
35          * See the {@link mw.Upload.Dialog upload dialog}.
36          *
37          * The {@link mw.Upload.BookletLayout.event:fileUploaded fileUploaded},
38          * and {@link mw.Upload.BookletLayout.event:fileSaved fileSaved} events can
39          * be used to get details of the upload.
40          *
41          * ## Extending
42          *
43          * To extend using {@link mw.Upload mw.Upload}, override
44          * {@link mw.Upload.BookletLayout#renderInfoForm renderInfoForm} to render
45          * the form required for the specific use-case. Update the
46          * {@link mw.Upload.BookletLayout#getFilename getFilename}, and
47          * {@link mw.Upload.BookletLayout#getText getText} methods to return data
48          * from your newly created form. If you added new fields you'll also have
49          * to update the {@link mw.Upload.BookletLayout#clear} method.
50          *
51          * If you plan to use a different upload model, apart from what is mentioned
52          * above, you'll also have to override the
53          * {@link mw.Upload.BookletLayout#createUpload createUpload} method to
54          * return the new model. The {@link #saveFile saveFile}, and
55          * the {@link mw.Upload.BookletLayout#uploadFile uploadFile} methods need to be
56          * overridden to use the new model and data returned from the forms.
57          *
58          * @class mw.Upload.BookletLayout
59          * @extends OO.ui.BookletLayout
60          *
61          * @constructor
62          * @description Create an instance of `mw.Upload.BookletLayout`.
63          * @param {Object} config Configuration options; see also the config parameter for the
64          *  {@link mw.Upload.BookletLayout} constructor.
65          * @param {jQuery} [config.$overlay] Overlay to use for widgets in the booklet
66          * @param {string} [config.filekey] Sets the stashed file to finish uploading. Overrides most of the file selection process, and fetches a thumbnail from the server.
67          */
68         mw.Upload.BookletLayout = function ( config ) {
69                 // Parent constructor
70                 mw.Upload.BookletLayout.super.call( this, config );
72                 this.$overlay = config.$overlay;
74                 this.filekey = config.filekey;
76                 this.renderUploadForm();
77                 this.renderInfoForm();
78                 this.renderInsertForm();
80                 this.addPages( [
81                         new OO.ui.PageLayout( 'initializing', {
82                                 scrollable: true,
83                                 padded: true,
84                                 content: [ new OO.ui.ProgressBarWidget( { indeterminate: true } ) ]
85                         } ),
86                         new OO.ui.PageLayout( 'upload', {
87                                 scrollable: true,
88                                 padded: true,
89                                 content: [ this.uploadForm ]
90                         } ),
91                         new OO.ui.PageLayout( 'info', {
92                                 scrollable: true,
93                                 padded: true,
94                                 content: [ this.infoForm ]
95                         } ),
96                         new OO.ui.PageLayout( 'insert', {
97                                 scrollable: true,
98                                 padded: true,
99                                 content: [ this.insertForm ]
100                         } )
101                 ] );
102         };
104         /* Setup */
106         OO.inheritClass( mw.Upload.BookletLayout, OO.ui.BookletLayout );
108         /* Events */
110         /**
111          * Progress events for the uploaded file.
112          *
113          * @event mw.Upload.BookletLayout.fileUploadProgress
114          * @param {number} progress In percentage
115          * @param {Object} duration Duration object from `moment.duration()`
116          */
118         /**
119          * The file has finished uploading.
120          *
121          * @event mw.Upload.BookletLayout.fileUploaded
122          */
124         /**
125          * The file has been saved to the database.
126          *
127          * @event mw.Upload.BookletLayout.fileSaved
128          * @param {Object} imageInfo See {@link mw.Upload#getImageInfo}
129          */
131         /**
132          * The upload form has changed.
133          *
134          * @event mw.Upload.BookletLayout.uploadValid
135          * @param {boolean} isValid The form is valid
136          */
138         /**
139          * The info form has changed.
140          *
141          * @event mw.Upload.BookletLayout.infoValid
142          * @param {boolean} isValid The form is valid
143          */
145         /* Properties */
147         /**
148          * The form rendered in the first step to get the file object.
149          * Rendered in {@link mw.Upload.BookletLayout#renderUploadForm renderUploadForm}.
150          *
151          * @name mw.Upload.BookletLayout.prototype.uploadForm
152          * @type {OO.ui.FormLayout}
153          */
155         /**
156          * The form rendered in the second step to get metadata.
157          * Rendered in {@link mw.Upload.BookletLayout#renderInfoForm renderInfoForm}.
158          *
159          * @name mw.Upload.BookletLayout.prototype.infoForm
160          * @type {OO.ui.FormLayout}
161          */
163         /**
164          * The form rendered in the third step to show usage.
165          * Rendered in {@link mw.Upload.BookletLayout#renderInsertForm renderInsertForm}.
166          *
167          * @name mw.Upload.BookletLayout.prototype.insertForm
168          * @type {OO.ui.FormLayout}
169          */
171         /* Methods */
173         /**
174          * Initialize for a new upload.
175          *
176          * @return {jQuery.Promise} Promise resolved when everything is initialized
177          */
178         mw.Upload.BookletLayout.prototype.initialize = function () {
179                 this.clear();
180                 this.upload = this.createUpload();
182                 this.setPage( 'initializing' );
184                 if ( this.filekey ) {
185                         this.setFilekey( this.filekey );
186                 }
188                 return this.upload.getApi().then(
189                         // If the user can't upload anything, don't give them the option to.
190                         ( api ) => api.getUserInfo().then(
191                                 ( userInfo ) => {
192                                         this.setPage( 'upload' );
193                                         if ( userInfo.rights.indexOf( 'upload' ) === -1 ) {
194                                                 if ( !mw.user.isNamed() ) {
195                                                         this.getPage( 'upload' ).$element.msg( 'apierror-mustbeloggedin', mw.msg( 'action-upload' ) );
196                                                 } else {
197                                                         this.getPage( 'upload' ).$element.msg( 'apierror-permissiondenied', mw.msg( 'action-upload' ) );
198                                                 }
199                                         }
200                                         return $.Deferred().resolve();
201                                 },
202                                 // Always resolve, never reject
203                                 () => {
204                                         this.setPage( 'upload' );
205                                         return $.Deferred().resolve();
206                                 }
207                         ),
208                         ( errorMsg ) => {
209                                 this.setPage( 'upload' );
210                                 // eslint-disable-next-line mediawiki/msg-doc
211                                 this.getPage( 'upload' ).$element.msg( errorMsg );
212                                 return $.Deferred().resolve();
213                         }
214                 );
215         };
217         /**
218          * Create a new upload model.
219          *
220          * @protected
221          * @return {mw.Upload} Upload model
222          */
223         mw.Upload.BookletLayout.prototype.createUpload = function () {
224                 return new mw.Upload( {
225                         parameters: {
226                                 errorformat: 'html',
227                                 errorlang: mw.config.get( 'wgUserLanguage' ),
228                                 errorsuselocal: 1,
229                                 formatversion: 2
230                         }
231                 } );
232         };
234         /* Uploading */
236         /**
237          * Uploads the file that was added in the upload form. Uses
238          * {@link mw.Upload.BookletLayout#getFile getFile} to get the HTML5
239          * file object.
240          *
241          * @protected
242          * @fires mw.Upload.BookletLayout.fileUploadProgress
243          * @fires mw.Upload.BookletLayout.fileUploaded
244          * @return {jQuery.Promise}
245          */
246         mw.Upload.BookletLayout.prototype.uploadFile = function () {
247                 const deferred = $.Deferred(),
248                         startTime = mw.now(),
249                         file = this.getFile();
251                 this.setPage( 'info' );
253                 if ( this.filekey ) {
254                         if ( file === null ) {
255                                 // Someone gonna get-a hurt real bad
256                                 throw new Error( 'filekey not passed into file select widget, which is impossible. Quitting while we\'re behind.' );
257                         }
259                         // Stashed file already uploaded.
260                         deferred.resolve();
261                         this.uploadPromise = deferred;
262                         this.emit( 'fileUploaded' );
263                         return deferred;
264                 }
266                 this.setFilename( file.name );
268                 this.upload.setFile( file );
269                 // The original file name might contain invalid characters, so use our sanitized one
270                 this.upload.setFilename( this.getFilename() );
272                 this.uploadPromise = this.upload.uploadToStash();
273                 this.uploadPromise.then( () => {
274                         deferred.resolve();
275                         this.emit( 'fileUploaded' );
276                 }, () => {
277                         // These errors will be thrown while the user is on the info page.
278                         this.getErrorMessageForStateDetails().then( ( errorMessage ) => {
279                                 deferred.reject( errorMessage );
280                         } );
281                 }, ( progress ) => {
282                         const elapsedTime = mw.now() - startTime,
283                                 estimatedTotalTime = ( 1 / progress ) * elapsedTime,
284                                 estimatedRemainingTime = moment.duration( estimatedTotalTime - elapsedTime );
285                         this.emit( 'fileUploadProgress', progress, estimatedRemainingTime );
286                 } );
288                 // If there is an error in uploading, come back to the upload page
289                 deferred.fail( () => {
290                         this.setPage( 'upload' );
291                 } );
293                 return deferred;
294         };
296         /**
297          * Saves the stash finalizes upload. Uses
298          * {@link mw.Upload.BookletLayout#getFilename getFilename}, and
299          * {@link mw.Upload.BookletLayout#getText getText} to get details from
300          * the form.
301          *
302          * @protected
303          * @fires mw.Upload.BookletLayout.fileSaved
304          * @return {jQuery.Promise} Rejects the promise with an
305          * {@link OO.ui.Error error}, or resolves if the upload was successful.
306          */
307         mw.Upload.BookletLayout.prototype.saveFile = function () {
308                 const deferred = $.Deferred();
310                 this.upload.setFilename( this.getFilename() );
311                 this.upload.setText( this.getText() );
313                 this.uploadPromise.then( () => {
314                         this.upload.finishStashUpload().then( () => {
315                                 // Normalize page name and localise the 'File:' prefix
316                                 const name = new mw.Title( 'File:' + this.upload.getFilename() ).toString();
317                                 this.filenameUsageWidget.setValue( '[[' + name + ']]' );
318                                 this.setPage( 'insert' );
320                                 deferred.resolve();
321                                 this.emit( 'fileSaved', this.upload.getImageInfo() );
322                         }, () => {
323                                 this.getErrorMessageForStateDetails().then( ( errorMessage ) => {
324                                         deferred.reject( errorMessage );
325                                 } );
326                         } );
327                 } );
329                 return deferred.promise();
330         };
332         /**
333          * Get an error message (as OO.ui.Error object) that should be displayed to the user for current
334          * state and state details.
335          *
336          * @protected
337          * @return {jQuery.Promise|undefined} A Promise that will be resolved with an OO.ui.Error.
338          */
339         mw.Upload.BookletLayout.prototype.getErrorMessageForStateDetails = function () {
340                 const state = this.upload.getState(),
341                         stateDetails = this.upload.getStateDetails(),
342                         warnings = stateDetails.upload && stateDetails.upload.warnings,
343                         $ul = $( '<ul>' );
345                 if ( state === mw.Upload.State.ERROR ) {
346                         const $error = ( new mw.Api() ).getErrorMessage( stateDetails );
348                         return $.Deferred().resolve( new OO.ui.Error(
349                                 $error,
350                                 { recoverable: false }
351                         ) );
352                 }
354                 if ( state === mw.Upload.State.WARNING ) {
355                         // We could get more than one of these errors, these are in order
356                         // of importance. For example fixing the thumbnail like file name
357                         // won't help the fact that the file already exists.
358                         if ( warnings.exists !== undefined ) {
359                                 return $.Deferred().resolve( new OO.ui.Error(
360                                         $( '<p>' ).msg( 'fileexists', 'File:' + warnings.exists ),
361                                         { recoverable: false }
362                                 ) );
363                         } else if ( warnings[ 'exists-normalized' ] !== undefined ) {
364                                 return $.Deferred().resolve( new OO.ui.Error(
365                                         $( '<p>' ).msg( 'fileexists', 'File:' + warnings[ 'exists-normalized' ] ),
366                                         { recoverable: false }
367                                 ) );
368                         } else if ( warnings[ 'page-exists' ] !== undefined ) {
369                                 return $.Deferred().resolve( new OO.ui.Error(
370                                         $( '<p>' ).msg( 'filepageexists', 'File:' + warnings[ 'page-exists' ] ),
371                                         { recoverable: false }
372                                 ) );
373                         } else if ( Array.isArray( warnings.duplicate ) ) {
374                                 warnings.duplicate.forEach( ( filename ) => {
375                                         const $a = $( '<a>' ).text( filename ),
376                                                 href = mw.Title.makeTitle( mw.config.get( 'wgNamespaceIds' ).file, filename ).getUrl( {} );
378                                         $a.attr( { href: href, target: '_blank' } );
379                                         $ul.append( $( '<li>' ).append( $a ) );
380                                 } );
382                                 return $.Deferred().resolve( new OO.ui.Error(
383                                         $( '<p>' ).msg( 'file-exists-duplicate', warnings.duplicate.length ).append( $ul ),
384                                         { recoverable: false }
385                                 ) );
386                         } else if ( warnings[ 'thumb-name' ] !== undefined ) {
387                                 return $.Deferred().resolve( new OO.ui.Error(
388                                         $( '<p>' ).msg( 'filename-thumb-name' ),
389                                         { recoverable: false }
390                                 ) );
391                         } else if ( warnings[ 'bad-prefix' ] !== undefined ) {
392                                 return $.Deferred().resolve( new OO.ui.Error(
393                                         $( '<p>' ).msg( 'filename-bad-prefix', warnings[ 'bad-prefix' ] ),
394                                         { recoverable: false }
395                                 ) );
396                         } else if ( warnings[ 'duplicate-archive' ] !== undefined ) {
397                                 return $.Deferred().resolve( new OO.ui.Error(
398                                         $( '<p>' ).msg( 'file-deleted-duplicate', 'File:' + warnings[ 'duplicate-archive' ] ),
399                                         { recoverable: false }
400                                 ) );
401                         } else if ( warnings[ 'was-deleted' ] !== undefined ) {
402                                 return $.Deferred().resolve( new OO.ui.Error(
403                                         $( '<p>' ).msg( 'filewasdeleted', 'File:' + warnings[ 'was-deleted' ] ),
404                                         { recoverable: false }
405                                 ) );
406                         } else if ( warnings.badfilename !== undefined ) {
407                                 // Change the name if the current name isn't acceptable
408                                 // TODO This might not really be the best place to do this
409                                 this.setFilename( warnings.badfilename );
410                                 return $.Deferred().resolve( new OO.ui.Error(
411                                         $( '<p>' ).msg( 'badfilename', warnings.badfilename )
412                                 ) );
413                         } else {
414                                 return $.Deferred().resolve( new OO.ui.Error(
415                                         // Let's get all the help we can if we can't pin point the error
416                                         $( '<p>' ).msg( 'api-error-unknown-warning', JSON.stringify( stateDetails ) ),
417                                         { recoverable: false }
418                                 ) );
419                         }
420                 }
421         };
423         /* Form renderers */
425         /**
426          * Renders and returns the upload form and sets the
427          * {@link mw.Upload.BookletLayout#uploadForm uploadForm} property.
428          *
429          * @protected
430          * @return {OO.ui.FormLayout}
431          */
432         mw.Upload.BookletLayout.prototype.renderUploadForm = function () {
433                 this.selectFileWidget = this.getFileWidget();
434                 const fieldset = new OO.ui.FieldsetLayout();
435                 fieldset.addItems( [ this.selectFileWidget ] );
436                 this.uploadForm = new OO.ui.FormLayout( { items: [ fieldset ] } );
438                 // Validation (if the SFW is for a stashed file, this never fires)
439                 this.selectFileWidget.on( 'change', this.onUploadFormChange.bind( this ) );
441                 this.selectFileWidget.on( 'change', () => {
442                         this.updateFilePreview();
443                 } );
445                 return this.uploadForm;
446         };
448         /**
449          * Gets the widget for displaying or inputting the file to upload.
450          *
451          * @return {OO.ui.SelectFileInputWidget|mw.widgets.StashedFileWidget}
452          */
453         mw.Upload.BookletLayout.prototype.getFileWidget = function () {
454                 if ( this.filekey ) {
455                         return new mw.widgets.StashedFileWidget( {
456                                 filekey: this.filekey
457                         } );
458                 }
460                 return new OO.ui.SelectFileInputWidget( {
461                         showDropTarget: true
462                 } );
463         };
465         /**
466          * Updates the file preview on the info form when a file is added.
467          *
468          * @protected
469          */
470         mw.Upload.BookletLayout.prototype.updateFilePreview = function () {
471                 this.selectFileWidget.loadAndGetImageUrl().done( ( url ) => {
472                         this.filePreview.$element.find( 'p' ).remove();
473                         this.filePreview.$element.css( 'background-image', 'url(' + url + ')' );
474                         this.infoForm.$element.addClass( 'mw-upload-bookletLayout-hasThumbnail' );
475                 } ).fail( () => {
476                         this.filePreview.$element.find( 'p' ).remove();
477                         if ( this.selectFileWidget.getValue() ) {
478                                 this.filePreview.$element.append(
479                                         $( '<p>' ).text( this.selectFileWidget.getValue().name )
480                                 );
481                         }
482                         this.filePreview.$element.css( 'background-image', '' );
483                         this.infoForm.$element.removeClass( 'mw-upload-bookletLayout-hasThumbnail' );
484                 } );
485         };
487         /**
488          * Handle change events to the upload form.
489          *
490          * @protected
491          * @fires mw.Upload.BookletLayout.uploadValid
492          */
493         mw.Upload.BookletLayout.prototype.onUploadFormChange = function () {
494                 this.emit( 'uploadValid', !!this.selectFileWidget.getValue() );
495         };
497         /**
498          * Renders and returns the information form for collecting
499          * metadata and sets the {@link mw.Upload.BookletLayout#infoForm infoForm}
500          * property.
501          *
502          * @protected
503          * @return {OO.ui.FormLayout}
504          */
505         mw.Upload.BookletLayout.prototype.renderInfoForm = function () {
506                 this.filePreview = new OO.ui.Widget( {
507                         classes: [ 'mw-upload-bookletLayout-filePreview' ]
508                 } );
509                 this.progressBarWidget = new OO.ui.ProgressBarWidget( {
510                         progress: 0
511                 } );
512                 this.filePreview.$element.append( this.progressBarWidget.$element );
514                 this.filenameWidget = new OO.ui.TextInputWidget( {
515                         indicator: 'required',
516                         required: true,
517                         validate: /.+/
518                 } );
519                 this.descriptionWidget = new OO.ui.MultilineTextInputWidget( {
520                         indicator: 'required',
521                         required: true,
522                         validate: /\S+/,
523                         autosize: true
524                 } );
526                 const fieldset = new OO.ui.FieldsetLayout( {
527                         label: mw.msg( 'upload-form-label-infoform-title' )
528                 } );
529                 fieldset.addItems( [
530                         new OO.ui.FieldLayout( this.filenameWidget, {
531                                 label: mw.msg( 'upload-form-label-infoform-name' ),
532                                 align: 'top',
533                                 help: mw.msg( 'upload-form-label-infoform-name-tooltip' )
534                         } ),
535                         new OO.ui.FieldLayout( this.descriptionWidget, {
536                                 label: mw.msg( 'upload-form-label-infoform-description' ),
537                                 align: 'top',
538                                 help: mw.msg( 'upload-form-label-infoform-description-tooltip' )
539                         } )
540                 ] );
541                 this.infoForm = new OO.ui.FormLayout( {
542                         classes: [ 'mw-upload-bookletLayout-infoForm' ],
543                         items: [ this.filePreview, fieldset ]
544                 } );
546                 this.on( 'fileUploadProgress', ( progress ) => {
547                         this.progressBarWidget.setProgress( progress * 100 );
548                 } );
550                 this.filenameWidget.on( 'change', this.onInfoFormChange.bind( this ) );
551                 this.descriptionWidget.on( 'change', this.onInfoFormChange.bind( this ) );
553                 return this.infoForm;
554         };
556         /**
557          * Handle change events to the info form.
558          *
559          * @protected
560          * @fires mw.Upload.BookletLayout.infoValid
561          */
562         mw.Upload.BookletLayout.prototype.onInfoFormChange = function () {
563                 $.when(
564                         this.filenameWidget.getValidity(),
565                         this.descriptionWidget.getValidity()
566                 ).done( () => {
567                         this.emit( 'infoValid', true );
568                 } ).fail( () => {
569                         this.emit( 'infoValid', false );
570                 } );
571         };
573         /**
574          * Renders and returns the insert form to show file usage and
575          * sets the {@link mw.Upload.BookletLayout#insertForm insertForm} property.
576          *
577          * @protected
578          * @return {OO.ui.FormLayout}
579          */
580         mw.Upload.BookletLayout.prototype.renderInsertForm = function () {
581                 this.filenameUsageWidget = new OO.ui.TextInputWidget();
582                 const fieldset = new OO.ui.FieldsetLayout( {
583                         label: mw.msg( 'upload-form-label-usage-title' )
584                 } );
585                 fieldset.addItems( [
586                         new OO.ui.FieldLayout( this.filenameUsageWidget, {
587                                 label: mw.msg( 'upload-form-label-usage-filename' ),
588                                 align: 'top'
589                         } )
590                 ] );
591                 this.insertForm = new OO.ui.FormLayout( { items: [ fieldset ] } );
593                 return this.insertForm;
594         };
596         /* Getters */
598         /**
599          * Gets the file object from the
600          * {@link mw.Upload.BookletLayout#uploadForm upload form}.
601          *
602          * @protected
603          * @return {File|null}
604          */
605         mw.Upload.BookletLayout.prototype.getFile = function () {
606                 return this.selectFileWidget.getValue();
607         };
609         /**
610          * Gets the file name from the
611          * {@link mw.Upload.BookletLayout#infoForm information form}.
612          *
613          * @protected
614          * @return {string}
615          */
616         mw.Upload.BookletLayout.prototype.getFilename = function () {
617                 let filename = this.filenameWidget.getValue();
618                 if ( this.filenameExtension ) {
619                         filename += '.' + this.filenameExtension;
620                 }
621                 return filename;
622         };
624         /**
625          * Prefills the {@link mw.Upload.BookletLayout#infoForm information form} with the given filename.
626          *
627          * @protected
628          * @param {string} filename
629          */
630         mw.Upload.BookletLayout.prototype.setFilename = function ( filename ) {
631                 const title = mw.Title.newFromFileName( filename );
633                 if ( title ) {
634                         this.filenameWidget.setValue( title.getNameText() );
635                         this.filenameExtension = mw.Title.normalizeExtension( title.getExtension() );
636                 } else {
637                         // Seems to happen for files with no extension, which should fail some checks anyway...
638                         this.filenameWidget.setValue( filename );
639                         this.filenameExtension = null;
640                 }
641         };
643         /**
644          * Gets the page text from the
645          * {@link mw.Upload.BookletLayout#infoForm information form}.
646          *
647          * @protected
648          * @return {string}
649          */
650         mw.Upload.BookletLayout.prototype.getText = function () {
651                 return this.descriptionWidget.getValue();
652         };
654         /* Setters */
656         /**
657          * Sets the file object.
658          *
659          * @protected
660          * @param {File|null} file File to select
661          */
662         mw.Upload.BookletLayout.prototype.setFile = function ( file ) {
663                 this.selectFileWidget.setValue( [ file ] );
664         };
666         /**
667          * Sets the filekey of a file already stashed on the server
668          * as the target of this upload operation.
669          *
670          * @protected
671          * @param {string} filekey
672          */
673         mw.Upload.BookletLayout.prototype.setFilekey = function ( filekey ) {
674                 this.upload.setFilekey( this.filekey );
675                 this.selectFileWidget.setValue( filekey );
677                 this.onUploadFormChange();
678         };
680         /**
681          * Clear the values of all fields.
682          *
683          * @protected
684          */
685         mw.Upload.BookletLayout.prototype.clear = function () {
686                 this.selectFileWidget.setValue( null );
687                 this.progressBarWidget.setProgress( 0 );
688                 this.filenameWidget.setValue( null ).setValidityFlag( true );
689                 this.descriptionWidget.setValue( null ).setValidityFlag( true );
690                 this.filenameUsageWidget.setValue( null );
691         };
693 }() );