844d74cc393024197bf7f23f152843314e414cb3
[lhc/web/wiklou.git] / resources / src / mediawiki / mediawiki.ForeignStructuredUpload.BookletLayout.js
1 /*global moment */
2 ( function ( $, mw ) {
3
4 /**
5 * mw.ForeignStructuredUpload.BookletLayout encapsulates the process
6 * of uploading a file to MediaWiki using the mw.ForeignStructuredUpload model.
7 *
8 * var uploadDialog = new mw.Upload.Dialog( {
9 * bookletClass: mw.ForeignStructuredUpload.BookletLayout,
10 * booklet: {
11 * target: 'local'
12 * }
13 * } );
14 * var windowManager = new OO.ui.WindowManager();
15 * $( 'body' ).append( windowManager.$element );
16 * windowManager.addWindows( [ uploadDialog ] );
17 *
18 * @class mw.ForeignStructuredUpload.BookletLayout
19 * @uses mw.ForeignStructuredUpload
20 * @extends mw.Upload.BookletLayout
21 * @cfg {string} [target] Used to choose the target repository.
22 * If nothing is passed, the {@link mw.ForeignUpload#property-target default} is used.
23 */
24 mw.ForeignStructuredUpload.BookletLayout = function ( config ) {
25 config = config || {};
26 // Parent constructor
27 mw.ForeignStructuredUpload.BookletLayout.parent.call( this, config );
28
29 this.target = config.target;
30 };
31
32 /* Setup */
33
34 OO.inheritClass( mw.ForeignStructuredUpload.BookletLayout, mw.Upload.BookletLayout );
35
36 /* Uploading */
37
38 /**
39 * @inheritdoc
40 */
41 mw.ForeignStructuredUpload.BookletLayout.prototype.initialize = function () {
42 var booklet = this;
43 return mw.ForeignStructuredUpload.BookletLayout.parent.prototype.initialize.call( this ).then(
44 function () {
45 return $.when(
46 // Point the CategorySelector to the right wiki
47 booklet.upload.getApi().then( function ( api ) {
48 // If this is a ForeignApi, it will have a apiUrl, otherwise we don't need to do anything
49 if ( api.apiUrl ) {
50 // Can't reuse the same object, CategorySelector calls #abort on its mw.Api instance
51 booklet.categoriesWidget.api = new mw.ForeignApi( api.apiUrl );
52 }
53 return $.Deferred().resolve();
54 } ),
55 // Set up booklet fields and license messages to match configuration
56 booklet.upload.loadConfig().then( function ( config ) {
57 var
58 msgPromise,
59 isLocal = booklet.upload.target === 'local',
60 fields = config.fields,
61 msgs = config.licensemessages[ isLocal ? 'local' : 'foreign' ];
62
63 // Hide disabled fields
64 booklet.descriptionField.toggle( !!fields.description );
65 booklet.categoriesField.toggle( !!fields.categories );
66 booklet.dateField.toggle( !!fields.date );
67 // Update form validity
68 booklet.onInfoFormChange();
69
70 // Load license messages from the remote wiki if we don't have these messages locally
71 // (this means that we only load messages from the foreign wiki for custom config)
72 if ( mw.message( 'upload-form-label-own-work-message-' + msgs ).exists() ) {
73 msgPromise = $.Deferred().resolve();
74 } else {
75 msgPromise = booklet.upload.apiPromise.then( function ( api ) {
76 return api.loadMessages( [
77 'upload-form-label-own-work-message-' + msgs,
78 'upload-form-label-not-own-work-message-' + msgs,
79 'upload-form-label-not-own-work-local-' + msgs
80 ] );
81 } );
82 }
83
84 // Update license messages
85 return msgPromise.then( function () {
86 var $labels;
87 booklet.$ownWorkMessage.msg( 'upload-form-label-own-work-message-' + msgs );
88 booklet.$notOwnWorkMessage.msg( 'upload-form-label-not-own-work-message-' + msgs );
89 booklet.$notOwnWorkLocal.msg( 'upload-form-label-not-own-work-local-' + msgs );
90
91 $labels = $( [
92 booklet.$ownWorkMessage[ 0 ],
93 booklet.$notOwnWorkMessage[ 0 ],
94 booklet.$notOwnWorkLocal[ 0 ]
95 ] );
96
97 // Improve the behavior of links inside these labels, which may point to important
98 // things like licensing requirements or terms of use
99 $labels.find( 'a' )
100 .attr( 'target', '_blank' )
101 .on( 'click', function ( e ) {
102 // OO.ui.FieldLayout#onLabelClick is trying to prevent default on all clicks,
103 // which causes the links to not be openable. Don't let it do that.
104 e.stopPropagation();
105 } );
106 } );
107 } )
108 );
109 }
110 ).then(
111 null,
112 // Always resolve, never reject
113 function () { return $.Deferred().resolve(); }
114 );
115 };
116
117 /**
118 * Returns a {@link mw.ForeignStructuredUpload mw.ForeignStructuredUpload}
119 * with the {@link #cfg-target target} specified in config.
120 *
121 * @protected
122 * @return {mw.Upload}
123 */
124 mw.ForeignStructuredUpload.BookletLayout.prototype.createUpload = function () {
125 return new mw.ForeignStructuredUpload( this.target );
126 };
127
128 /* Form renderers */
129
130 /**
131 * @inheritdoc
132 */
133 mw.ForeignStructuredUpload.BookletLayout.prototype.renderUploadForm = function () {
134 var fieldset,
135 layout = this;
136
137 // These elements are filled with text in #initialize
138 // TODO Refactor this to be in one place
139 this.$ownWorkMessage = $( '<p>' )
140 .addClass( 'mw-foreignStructuredUpload-bookletLayout-license' );
141 this.$notOwnWorkMessage = $( '<p>' );
142 this.$notOwnWorkLocal = $( '<p>' );
143
144 this.selectFileWidget = new OO.ui.SelectFileWidget( {
145 showDropTarget: true
146 } );
147 this.messageLabel = new OO.ui.LabelWidget( {
148 label: $( '<div>' ).append(
149 this.$notOwnWorkMessage,
150 this.$notOwnWorkLocal
151 )
152 } );
153 this.ownWorkCheckbox = new OO.ui.CheckboxInputWidget().on( 'change', function ( on ) {
154 layout.messageLabel.toggle( !on );
155 } );
156
157 fieldset = new OO.ui.FieldsetLayout();
158 fieldset.addItems( [
159 new OO.ui.FieldLayout( this.selectFileWidget, {
160 align: 'top'
161 } ),
162 new OO.ui.FieldLayout( this.ownWorkCheckbox, {
163 align: 'inline',
164 label: $( '<div>' ).append(
165 $( '<p>' ).text( mw.msg( 'upload-form-label-own-work' ) ),
166 this.$ownWorkMessage
167 )
168 } ),
169 new OO.ui.FieldLayout( this.messageLabel, {
170 align: 'top'
171 } )
172 ] );
173 this.uploadForm = new OO.ui.FormLayout( { items: [ fieldset ] } );
174
175 // Validation
176 this.selectFileWidget.on( 'change', this.onUploadFormChange.bind( this ) );
177 this.ownWorkCheckbox.on( 'change', this.onUploadFormChange.bind( this ) );
178
179 this.selectFileWidget.on( 'change', function () {
180 var file = layout.getFile();
181
182 // Set the date to lastModified once we have the file
183 if ( layout.getDateFromLastModified( file ) !== undefined ) {
184 layout.dateWidget.setValue( layout.getDateFromLastModified( file ) );
185 }
186
187 // Check if we have EXIF data and set to that where available
188 layout.getDateFromExif( file ).done( function ( date ) {
189 layout.dateWidget.setValue( date );
190 } );
191
192 layout.updateFilePreview();
193 } );
194
195 return this.uploadForm;
196 };
197
198 /**
199 * @inheritdoc
200 */
201 mw.ForeignStructuredUpload.BookletLayout.prototype.onUploadFormChange = function () {
202 var file = this.selectFileWidget.getValue(),
203 ownWork = this.ownWorkCheckbox.isSelected(),
204 valid = !!file && ownWork;
205 this.emit( 'uploadValid', valid );
206 };
207
208 /**
209 * @inheritdoc
210 */
211 mw.ForeignStructuredUpload.BookletLayout.prototype.renderInfoForm = function () {
212 var fieldset;
213
214 this.filePreview = new OO.ui.Widget( {
215 classes: [ 'mw-upload-bookletLayout-filePreview' ]
216 } );
217 this.progressBarWidget = new OO.ui.ProgressBarWidget( {
218 progress: 0
219 } );
220 this.filePreview.$element.append( this.progressBarWidget.$element );
221
222 this.filenameWidget = new OO.ui.TextInputWidget( {
223 required: true,
224 validate: /.+/
225 } );
226 this.descriptionWidget = new OO.ui.TextInputWidget( {
227 required: true,
228 validate: /\S+/,
229 multiline: true,
230 autosize: true
231 } );
232 this.categoriesWidget = new mw.widgets.CategorySelector( {
233 // Can't be done here because we don't know the target wiki yet... done in #initialize.
234 // api: new mw.ForeignApi( ... ),
235 $overlay: this.$overlay
236 } );
237 this.dateWidget = new mw.widgets.DateInputWidget( {
238 $overlay: this.$overlay,
239 required: true,
240 mustBeBefore: moment().add( 1, 'day' ).locale( 'en' ).format( 'YYYY-MM-DD' ) // Tomorrow
241 } );
242
243 this.filenameField = new OO.ui.FieldLayout( this.filenameWidget, {
244 label: mw.msg( 'upload-form-label-infoform-name' ),
245 align: 'top',
246 classes: [ 'mw-foreignStructuredUploa-bookletLayout-small-notice' ],
247 notices: [ mw.msg( 'upload-form-label-infoform-name-tooltip' ) ]
248 } );
249 this.descriptionField = new OO.ui.FieldLayout( this.descriptionWidget, {
250 label: mw.msg( 'upload-form-label-infoform-description' ),
251 align: 'top',
252 classes: [ 'mw-foreignStructuredUploa-bookletLayout-small-notice' ],
253 notices: [ mw.msg( 'upload-form-label-infoform-description-tooltip' ) ]
254 } );
255 this.categoriesField = new OO.ui.FieldLayout( this.categoriesWidget, {
256 label: mw.msg( 'upload-form-label-infoform-categories' ),
257 align: 'top'
258 } );
259 this.dateField = new OO.ui.FieldLayout( this.dateWidget, {
260 label: mw.msg( 'upload-form-label-infoform-date' ),
261 align: 'top'
262 } );
263
264 fieldset = new OO.ui.FieldsetLayout( {
265 label: mw.msg( 'upload-form-label-infoform-title' )
266 } );
267 fieldset.addItems( [
268 this.filenameField,
269 this.descriptionField,
270 this.categoriesField,
271 this.dateField
272 ] );
273 this.infoForm = new OO.ui.FormLayout( {
274 classes: [ 'mw-upload-bookletLayout-infoForm' ],
275 items: [ this.filePreview, fieldset ]
276 } );
277
278 // Validation
279 this.filenameWidget.on( 'change', this.onInfoFormChange.bind( this ) );
280 this.descriptionWidget.on( 'change', this.onInfoFormChange.bind( this ) );
281 this.dateWidget.on( 'change', this.onInfoFormChange.bind( this ) );
282
283 this.on( 'fileUploadProgress', function ( progress ) {
284 this.progressBarWidget.setProgress( progress * 100 );
285 }.bind( this ) );
286
287 return this.infoForm;
288 };
289
290 /**
291 * @inheritdoc
292 */
293 mw.ForeignStructuredUpload.BookletLayout.prototype.onInfoFormChange = function () {
294 var layout = this,
295 validityPromises = [];
296
297 validityPromises.push( this.filenameWidget.getValidity() );
298 if ( this.descriptionField.isVisible() ) {
299 validityPromises.push( this.descriptionWidget.getValidity() );
300 }
301 if ( this.dateField.isVisible() ) {
302 validityPromises.push( this.dateWidget.getValidity() );
303 }
304
305 $.when.apply( $, validityPromises ).done( function () {
306 layout.emit( 'infoValid', true );
307 } ).fail( function () {
308 layout.emit( 'infoValid', false );
309 } );
310 };
311
312 /**
313 * @param {mw.Title} filename
314 * @return {jQuery.Promise} Resolves (on success) or rejects with OO.ui.Error
315 */
316 mw.ForeignStructuredUpload.BookletLayout.prototype.validateFilename = function ( filename ) {
317 return ( new mw.Api() ).get( {
318 action: 'query',
319 prop: 'info',
320 titles: filename.getPrefixedDb(),
321 formatversion: 2
322 } ).then(
323 function ( result ) {
324 // if the file already exists, reject right away, before
325 // ever firing finishStashUpload()
326 if ( !result.query.pages[ 0 ].missing ) {
327 return $.Deferred().reject( new OO.ui.Error(
328 $( '<p>' ).msg( 'fileexists', filename.getPrefixedDb() ),
329 { recoverable: false }
330 ) );
331 }
332 },
333 function () {
334 // API call failed - this could be a connection hiccup...
335 // Let's just ignore this validation step and turn this
336 // failure into a successful resolve ;)
337 return $.Deferred().resolve();
338 }
339 );
340 };
341
342 /**
343 * @inheritdoc
344 */
345 mw.ForeignStructuredUpload.BookletLayout.prototype.saveFile = function () {
346 var title = mw.Title.newFromText(
347 this.getFilename(),
348 mw.config.get( 'wgNamespaceIds' ).file
349 );
350
351 return this.uploadPromise
352 .then( this.validateFilename.bind( this, title ) )
353 .then( mw.ForeignStructuredUpload.BookletLayout.parent.prototype.saveFile.bind( this ) );
354 };
355
356 /* Getters */
357
358 /**
359 * @inheritdoc
360 */
361 mw.ForeignStructuredUpload.BookletLayout.prototype.getText = function () {
362 var language = mw.config.get( 'wgContentLanguage' );
363 this.upload.clearDescriptions();
364 this.upload.addDescription( language, this.descriptionWidget.getValue() );
365 this.upload.setDate( this.dateWidget.getValue() );
366 this.upload.clearCategories();
367 this.upload.addCategories( this.categoriesWidget.getItemsData() );
368 return this.upload.getText();
369 };
370
371 /**
372 * Get original date from EXIF data
373 *
374 * @param {Object} file
375 * @return {jQuery.Promise} Promise resolved with the EXIF date
376 */
377 mw.ForeignStructuredUpload.BookletLayout.prototype.getDateFromExif = function ( file ) {
378 var fileReader,
379 deferred = $.Deferred();
380
381 if ( file && file.type === 'image/jpeg' ) {
382 fileReader = new FileReader();
383 fileReader.onload = function () {
384 var fileStr, arr, i, metadata;
385
386 if ( typeof fileReader.result === 'string' ) {
387 fileStr = fileReader.result;
388 } else {
389 // Array buffer; convert to binary string for the library.
390 arr = new Uint8Array( fileReader.result );
391 fileStr = '';
392 for ( i = 0; i < arr.byteLength; i++ ) {
393 fileStr += String.fromCharCode( arr[ i ] );
394 }
395 }
396
397 try {
398 metadata = mw.libs.jpegmeta( this.result, file.name );
399 } catch ( e ) {
400 metadata = null;
401 }
402
403 if ( metadata !== null && metadata.exif !== undefined && metadata.exif.DateTimeOriginal ) {
404 deferred.resolve( moment( metadata.exif.DateTimeOriginal, 'YYYY:MM:DD' ).format( 'YYYY-MM-DD' ) );
405 } else {
406 deferred.reject();
407 }
408 };
409
410 if ( 'readAsBinaryString' in fileReader ) {
411 fileReader.readAsBinaryString( file );
412 } else if ( 'readAsArrayBuffer' in fileReader ) {
413 fileReader.readAsArrayBuffer( file );
414 } else {
415 // We should never get here
416 deferred.reject();
417 throw new Error( 'Cannot read thumbnail as binary string or array buffer.' );
418 }
419 }
420
421 return deferred.promise();
422 };
423
424 /**
425 * Get last modified date from file
426 *
427 * @param {Object} file
428 * @return {Object} Last modified date from file
429 */
430 mw.ForeignStructuredUpload.BookletLayout.prototype.getDateFromLastModified = function ( file ) {
431 if ( file && file.lastModified ) {
432 return moment( file.lastModified ).format( 'YYYY-MM-DD' );
433 }
434 };
435
436 /* Setters */
437
438 /**
439 * @inheritdoc
440 */
441 mw.ForeignStructuredUpload.BookletLayout.prototype.clear = function () {
442 mw.ForeignStructuredUpload.BookletLayout.parent.prototype.clear.call( this );
443
444 this.ownWorkCheckbox.setSelected( false );
445 this.categoriesWidget.setItemsFromData( [] );
446 this.dateWidget.setValue( '' ).setValidityFlag( true );
447 };
448
449 }( jQuery, mediaWiki ) );