Add mw.Upload.Dialog as a UI to mw.Upload
[lhc/web/wiklou.git] / resources / src / mediawiki / mediawiki.Upload.Dialog.js
1 ( function ( $, mw ) {
2
3 /**
4 * mw.Upload.Dialog encapsulates the process of uploading a file
5 * to MediaWiki using the {@link mw.Upload mw.Upload} model.
6 * The dialog 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.ProcessDialog dialog} has three steps-
14 *
15 * - **Upload**: Has a {@link OO.ui.SelectFileWidget field} to get the
16 * file object.
17 *
18 * - **Information**: Has a {@link OO.ui.FormLayout form} to
19 * collect metadata. This can be 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.Dialog#renderUploadForm renderUploadForm},
25 * {@link mw.Upload.Dialog#renderInfoForm renderInfoForm}, and
26 * {@link mw.Upload.Dialog#renderInsertForm renderInfoForm}. The
27 * {@link mw.Upload.Dialog#getFile getFile},
28 * {@link mw.Upload.Dialog#getFilename getFilename}, and
29 * {@link mw.Upload.Dialog#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 * To use, setup a {@link OO.ui.WindowManager window manager} like for normal
36 * dialogs-
37 *
38 * var uploadDialog = new mw.Upload.Dialog( { size: 'small' } );
39 * var windowManager = new OO.ui.WindowManager();
40 * $( 'body' ).append( windowManager.$element );
41 * windowManager.addWindows( [ uploadDialog ] );
42 * windowManager.openWindow( uploadDialog );
43 *
44 * The dialog's closing promise,
45 * {@link mw.Upload.Dialog#event-fileUploaded fileUploaded},
46 * and {@link mw.Upload.Dialog#event-fileSaved fileSaved} events can
47 * be used to get details of the upload
48 *
49 * ## Extending
50 *
51 * To extend using {@link mw.Upload mw.Upload}, override
52 * {@link mw.Upload.Dialog#renderInfoForm renderInfoForm} to render
53 * the form required for the specific use-case. Update the
54 * {@link mw.Upload.Dialog#getFilename getFilename}, and
55 * {@link mw.Upload.Dialog#getText getText} methods to return data
56 * from your newly created form. If you added new fields you'll also have
57 * to update the {@link #getTeardownProcess} method.
58 *
59 * If you plan to use a different upload model, apart from what is mentioned
60 * above, you'll also have to override the
61 * {@link mw.Upload.Dialog#getUploadObject getUploadObject} method to
62 * return the new model. The {@link mw.Upload.Dialog#saveFile saveFile}, and
63 * the {@link mw.Upload.Dialog#uploadFile uploadFile} methods need to be
64 * overriden to use the new model and data returned from the forms.
65 *
66 * @class mw.Upload.Dialog
67 * @uses mw.Upload
68 * @extends OO.ui.ProcessDialog
69 */
70 mw.Upload.Dialog = function ( config ) {
71 // Parent constructor
72 mw.Upload.Dialog.parent.call( this, config );
73 };
74
75 /* Setup */
76
77 OO.inheritClass( mw.Upload.Dialog, OO.ui.ProcessDialog );
78
79 /* Static Properties */
80
81 /**
82 * @inheritdoc
83 * @property title
84 */
85 /*jshint -W024*/
86 mw.Upload.Dialog.static.title = mw.msg( 'upload-dialog-title' );
87
88 /**
89 * @inheritdoc
90 * @property actions
91 */
92 mw.Upload.Dialog.static.actions = [
93 {
94 flags: 'safe',
95 action: 'cancel',
96 label: mw.msg( 'upload-dialog-button-cancel' ),
97 modes: [ 'upload', 'insert', 'save' ]
98 },
99 {
100 flags: [ 'primary', 'progressive' ],
101 label: mw.msg( 'upload-dialog-button-done' ),
102 action: 'insert',
103 modes: 'insert'
104 },
105 {
106 flags: [ 'primary', 'constructive' ],
107 label: mw.msg( 'upload-dialog-button-save' ),
108 action: 'save',
109 modes: 'save'
110 },
111 {
112 flags: [ 'primary', 'progressive' ],
113 label: mw.msg( 'upload-dialog-button-upload' ),
114 action: 'upload',
115 modes: 'upload'
116 }
117 ];
118 /*jshint +W024*/
119
120 /* Properties */
121
122 /**
123 * @property {OO.ui.FormLayout} uploadForm
124 * The form rendered in the first step to get the file object.
125 * Rendered in {@link mw.Upload.Dialog#renderUploadForm renderUploadForm}.
126 */
127
128 /**
129 * @property {OO.ui.FormLayout} infoForm
130 * The form rendered in the second step to get metadata.
131 * Rendered in {@link mw.Upload.Dialog#renderInfoForm renderInfoForm}
132 */
133
134 /**
135 * @property {OO.ui.FormLayout} insertForm
136 * The form rendered in the third step to show usage
137 * Rendered in {@link mw.Upload.Dialog#renderInsertForm renderInsertForm}
138 */
139
140 /* Events */
141
142 /**
143 * A `fileUploaded` event is emitted from the
144 * {@link mw.Upload.Dialog#uploadFile uploadFile} method.
145 *
146 * @event fileUploaded
147 */
148
149 /**
150 * A `fileSaved` event is emitted from the
151 * {@link mw.Upload.Dialog#saveFile saveFile} method.
152 *
153 * @event fileSaved
154 */
155
156 /* Methods */
157
158 /**
159 * @inheritdoc
160 */
161 mw.Upload.Dialog.prototype.initialize = function () {
162 mw.Upload.Dialog.parent.prototype.initialize.call( this );
163
164 this.renderUploadForm();
165 this.renderInfoForm();
166 this.renderInsertForm();
167
168 this.uploadFormPanel = new OO.ui.PanelLayout( {
169 scrollable: true,
170 padded: true,
171 content: [ this.uploadForm ]
172 } );
173 this.infoFormPanel = new OO.ui.PanelLayout( {
174 scrollable: true,
175 padded: true,
176 content: [ this.infoForm ]
177 } );
178 this.insertFormPanel = new OO.ui.PanelLayout( {
179 scrollable: true,
180 padded: true,
181 content: [ this.insertForm ]
182 } );
183
184 this.panels = new OO.ui.StackLayout();
185 this.panels.addItems( [
186 this.uploadFormPanel,
187 this.infoFormPanel,
188 this.insertFormPanel
189 ] );
190
191 this.$body.append( this.panels.$element );
192 };
193
194 /**
195 * @inheritdoc
196 */
197 mw.Upload.Dialog.prototype.getBodyHeight = function () {
198 return 300;
199 };
200
201 /**
202 * Switch between the panels.
203 *
204 * @param {string} panel Panel name: 'upload', 'info', 'insert'
205 */
206 mw.Upload.Dialog.prototype.switchPanels = function ( panel ) {
207 switch ( panel ) {
208 case 'upload':
209 this.panels.setItem( this.uploadFormPanel );
210 this.actions.setMode( 'upload' );
211 break;
212 case 'info':
213 this.panels.setItem( this.infoFormPanel );
214 this.actions.setMode( 'save' );
215 break;
216 case 'insert':
217 this.panels.setItem( this.insertFormPanel );
218 this.actions.setMode( 'insert' );
219 break;
220 }
221 };
222
223 /**
224 * @inheritdoc
225 */
226 mw.Upload.Dialog.prototype.getSetupProcess = function ( data ) {
227 return mw.Upload.Dialog.parent.prototype.getSetupProcess.call( this, data )
228 .next( function () {
229 this.upload = this.getUploadObject();
230 this.switchPanels( 'upload' );
231 this.actions.setAbilities( { upload: false } );
232 }, this );
233 };
234
235 /**
236 * @inheritdoc
237 */
238 mw.Upload.Dialog.prototype.getActionProcess = function ( action ) {
239 var dialog = this;
240
241 if ( action === 'upload' ) {
242 return new OO.ui.Process( function () {
243 dialog.filenameWidget.setValue( dialog.getFile().name );
244 dialog.switchPanels( 'info' );
245 dialog.actions.setAbilities( { save: false } );
246 return dialog.uploadFile();
247 } );
248 }
249 if ( action === 'save' ) {
250 return new OO.ui.Process( dialog.saveFile() );
251 }
252 if ( action === 'insert' ) {
253 return new OO.ui.Process( function () {
254 dialog.close( dialog.upload );
255 } );
256 }
257 if ( action === 'cancel' ) {
258 return new OO.ui.Process( dialog.close() );
259 }
260
261 return mw.Upload.Dialog.parent.prototype.getActionProcess.call( this, action );
262 };
263
264 /**
265 * @inheritdoc
266 */
267 mw.Upload.Dialog.prototype.getTeardownProcess = function ( data ) {
268 return mw.Upload.Dialog.parent.prototype.getTeardownProcess.call( this, data )
269 .next( function () {
270 // Clear the values of all fields
271 this.selectFileWidget.setValue( null );
272 this.filenameWidget.setValue( null ).setValidityFlag( true );
273 this.descriptionWidget.setValue( null ).setValidityFlag( true );
274 this.filenameUsageWidget.setValue( null );
275 }, this );
276 };
277
278 /* Uploading */
279
280 /**
281 * Get the upload model object required for this dialog. Can be
282 * extended to different models.
283 *
284 * @return {mw.Upload}
285 */
286 mw.Upload.Dialog.prototype.getUploadObject = function () {
287 return new mw.Upload();
288 };
289
290 /**
291 * Uploads the file that was added in the upload form. Uses
292 * {@link mw.Upload.Dialog#getFile getFile} to get the HTML5
293 * file object.
294 *
295 * @protected
296 * @fires fileUploaded
297 * @return {jQuery.Promise}
298 */
299 mw.Upload.Dialog.prototype.uploadFile = function () {
300 var dialog = this,
301 file = this.getFile();
302 this.upload.setFile( file );
303 this.uploadPromise = this.upload.uploadToStash();
304 this.uploadPromise.then( function () {
305 dialog.emit( 'fileUploaded' );
306 } );
307
308 return this.uploadPromise;
309 };
310
311 /**
312 * Saves the stash finalizes upload. Uses
313 * {@link mw.Upload.Dialog#getFilename getFilename}, and
314 * {@link mw.Upload.Dialog#getText getText} to get details from
315 * the form.
316 *
317 * @protected
318 * @fires fileSaved
319 * @returns {jQuery.Promise} Rejects the promise with an
320 * {@link OO.ui.Error error}, or resolves if the upload was successful.
321 */
322 mw.Upload.Dialog.prototype.saveFile = function () {
323 var dialog = this,
324 promise = $.Deferred();
325
326 this.upload.setFilename( this.getFilename() );
327 this.upload.setText( this.getText() );
328
329 this.uploadPromise.always( function () {
330
331 if ( dialog.upload.getState() === mw.Upload.State.ERROR ) {
332 promise.reject( new OO.ui.Error( mw.msg( 'upload-dialog-error' ) ) );
333 return false;
334 }
335
336 if ( dialog.upload.getState() === mw.Upload.State.WARNING ) {
337 promise.reject( new OO.ui.Error( mw.msg( 'upload-dialog-error' ) ) );
338 return false;
339 }
340
341 dialog.upload.finishStashUpload().then( function () {
342 var name;
343
344 if ( dialog.upload.getState() === mw.Upload.State.ERROR ) {
345 promise.reject( new OO.ui.Error( mw.msg( 'upload-dialog-error' ) ) );
346 return false;
347 }
348
349 if ( dialog.upload.getState() === mw.Upload.State.WARNING ) {
350 promise.reject( new OO.ui.Error( mw.msg( 'upload-dialog-warning' ) ) );
351 return false;
352 }
353
354 // Normalize page name and localise the 'File:' prefix
355 name = new mw.Title( 'File:' + dialog.upload.getFilename() ).toString();
356 dialog.filenameUsageWidget.setValue( '[[' + name + ']]' );
357 dialog.switchPanels( 'insert' );
358
359 promise.resolve();
360 dialog.emit( 'fileSaved' );
361 } );
362 } );
363
364 return promise.promise();
365 };
366
367 /* Form renderers */
368
369 /**
370 * Renders and returns the upload form and sets the
371 * {@link mw.Upload.Dialog#uploadForm uploadForm} property.
372 * Validates the form and
373 * {@link OO.ui.ActionSet#setAbilities sets abilities}
374 * for the dialog accordingly.
375 *
376 * @protected
377 * @returns {OO.ui.FormLayout}
378 */
379 mw.Upload.Dialog.prototype.renderUploadForm = function () {
380 var fieldset,
381 dialog = this;
382
383 this.selectFileWidget = new OO.ui.SelectFileWidget();
384 fieldset = new OO.ui.FieldsetLayout( { label: mw.msg( 'upload-dialog-label-select-file' ) } );
385 fieldset.addItems( [ this.selectFileWidget ] );
386 this.uploadForm = new OO.ui.FormLayout( { items: [ fieldset ] } );
387
388 // Validation
389 this.selectFileWidget.on( 'change', function ( value ) {
390 dialog.actions.setAbilities( { upload: !!value } );
391 } );
392
393 return this.uploadForm;
394 };
395
396 /**
397 * Renders and returns the information form for collecting
398 * metadata and sets the {@link mw.Upload.Dialog#infoForm infoForm}
399 * property.
400 * Validates the form and
401 * {@link OO.ui.ActionSet#setAbilities sets abilities}
402 * for the dialog accordingly.
403 *
404 * @protected
405 * @returns {OO.ui.FormLayout}
406 */
407 mw.Upload.Dialog.prototype.renderInfoForm = function () {
408 var fieldset,
409 dialog = this;
410
411 this.filenameWidget = new OO.ui.TextInputWidget( {
412 indicator: 'required',
413 required: true,
414 validate: /.+/
415 } );
416 this.descriptionWidget = new OO.ui.TextInputWidget( {
417 indicator: 'required',
418 required: true,
419 validate: /.+/,
420 multiline: true,
421 autosize: true
422 } );
423
424 fieldset = new OO.ui.FieldsetLayout( {
425 label: mw.msg( 'upload-dialog-label-infoform-title' )
426 } );
427 fieldset.addItems( [
428 new OO.ui.FieldLayout( this.filenameWidget, {
429 label: mw.msg( 'upload-dialog-label-infoform-name' ),
430 align: 'top'
431 } ),
432 new OO.ui.FieldLayout( this.descriptionWidget, {
433 label: mw.msg( 'upload-dialog-label-infoform-description' ),
434 align: 'top'
435 } )
436 ] );
437 this.infoForm = new OO.ui.FormLayout( { items: [ fieldset ] } );
438
439 // Validation
440 function checkValidity() {
441 var validityPromises = [
442 dialog.filenameWidget.isValid(),
443 dialog.descriptionWidget.isValid()
444 ];
445
446 $.when.apply( $, validityPromises ).done( function () {
447 var allValid,
448 values = Array.prototype.slice.apply( arguments );
449 allValid = values.every( function ( value ) {
450 return value;
451 } );
452
453 dialog.actions.setAbilities( { save: allValid } );
454 } );
455 }
456 this.filenameWidget.on( 'change', checkValidity );
457 this.descriptionWidget.on( 'change', checkValidity );
458
459 return this.infoForm;
460 };
461
462 /**
463 * Renders and returns the insert form to show file usage and
464 * sets the {@link mw.Upload.Dialog#insertForm insertForm} property.
465 *
466 * @protected
467 * @returns {OO.ui.FormLayout}
468 */
469 mw.Upload.Dialog.prototype.renderInsertForm = function () {
470 var fieldset;
471
472 this.filenameUsageWidget = new OO.ui.TextInputWidget();
473 fieldset = new OO.ui.FieldsetLayout( {
474 label: mw.msg( 'upload-dialog-label-usage-title' )
475 } );
476 fieldset.addItems( [
477 new OO.ui.FieldLayout( this.filenameUsageWidget, {
478 label: mw.msg( 'upload-dialog-label-usage-filename' ),
479 align: 'top'
480 } )
481 ] );
482 this.insertForm = new OO.ui.FormLayout( { items: [ fieldset ] } );
483
484 return this.insertForm;
485 };
486
487 /* Getters */
488
489 /**
490 * Gets the file object from the
491 * {@link mw.Upload.Dialog#uploadForm upload form}.
492 *
493 * @protected
494 * @returns {File|null}
495 */
496 mw.Upload.Dialog.prototype.getFile = function () {
497 return this.selectFileWidget.getValue();
498 };
499
500 /**
501 * Gets the file name from the
502 * {@link mw.Upload.Dialog#infoForm information form}.
503 *
504 * @protected
505 * @returns {string}
506 */
507 mw.Upload.Dialog.prototype.getFilename = function () {
508 return this.filenameWidget.getValue();
509 };
510
511 /**
512 * Gets the page text from the
513 * {@link mw.Upload.Dialog#infoForm information form}.
514 *
515 * @protected
516 * @returns {string}
517 */
518 mw.Upload.Dialog.prototype.getText = function () {
519 return this.descriptionWidget.getValue();
520 };
521 }( jQuery, mediaWiki ) );