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