1 /* global moment, Uint8Array */
5 * mw.ForeignStructuredUpload.BookletLayout encapsulates the process
6 * of uploading a file to MediaWiki using the mw.ForeignStructuredUpload model.
8 * var uploadDialog = new mw.Upload.Dialog( {
9 * bookletClass: mw.ForeignStructuredUpload.BookletLayout,
14 * var windowManager = new OO.ui.WindowManager();
15 * $( 'body' ).append( windowManager.$element );
16 * windowManager.addWindows( [ uploadDialog ] );
18 * @class mw.ForeignStructuredUpload.BookletLayout
19 * @uses mw.ForeignStructuredUpload
20 * @extends mw.Upload.BookletLayout
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.
27 mw
.ForeignStructuredUpload
.BookletLayout = function ( config
) {
28 config
= config
|| {};
30 mw
.ForeignStructuredUpload
.BookletLayout
.parent
.call( this, config
);
32 this.target
= config
.target
;
37 OO
.inheritClass( mw
.ForeignStructuredUpload
.BookletLayout
, mw
.Upload
.BookletLayout
);
44 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.initialize = function () {
46 return mw
.ForeignStructuredUpload
.BookletLayout
.parent
.prototype.initialize
.call( this ).then(
49 // Point the CategorySelector 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
53 // Can't reuse the same object, CategorySelector calls #abort on its mw.Api instance
54 booklet
.categoriesWidget
.api
= new mw
.ForeignApi( api
.apiUrl
);
56 return $.Deferred().resolve();
58 // Set up booklet fields and license messages to match configuration
59 booklet
.upload
.loadConfig().then( function ( config
) {
62 isLocal
= booklet
.upload
.target
=== 'local',
63 fields
= config
.fields
,
64 msgs
= config
.licensemessages
[ isLocal
? 'local' : 'foreign' ];
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();
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();
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
87 // Update license messages
88 return msgPromise
.then( function () {
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
);
95 booklet
.$ownWorkMessage
[ 0 ],
96 booklet
.$notOwnWorkMessage
[ 0 ],
97 booklet
.$notOwnWorkLocal
[ 0 ]
100 // Improve the behavior of links inside these labels, which may point to important
101 // things like licensing requirements or terms of use
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.
110 }, function ( errorMsg
) {
111 booklet
.getPage( 'upload' ).$element
.msg( errorMsg
);
112 return $.Deferred().resolve();
118 // Always resolve, never reject
119 function () { return $.Deferred().resolve(); }
124 * Returns a {@link mw.ForeignStructuredUpload mw.ForeignStructuredUpload}
125 * with the {@link #cfg-target target} specified in config.
128 * @return {mw.Upload}
130 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.createUpload = function () {
131 return new mw
.ForeignStructuredUpload( this.target
);
139 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.renderUploadForm = function () {
143 // These elements are filled with text in #initialize
144 // TODO Refactor this to be in one place
145 this.$ownWorkMessage
= $( '<p>' )
146 .addClass( 'mw-foreignStructuredUpload-bookletLayout-license' );
147 this.$notOwnWorkMessage
= $( '<p>' );
148 this.$notOwnWorkLocal
= $( '<p>' );
150 this.selectFileWidget
= new OO
.ui
.SelectFileWidget( {
153 this.messageLabel
= new OO
.ui
.LabelWidget( {
154 label
: $( '<div>' ).append(
155 this.$notOwnWorkMessage
,
156 this.$notOwnWorkLocal
159 this.ownWorkCheckbox
= new OO
.ui
.CheckboxInputWidget().on( 'change', function ( on
) {
160 layout
.messageLabel
.toggle( !on
);
163 fieldset
= new OO
.ui
.FieldsetLayout();
165 new OO
.ui
.FieldLayout( this.selectFileWidget
, {
168 new OO
.ui
.FieldLayout( this.ownWorkCheckbox
, {
170 label
: $( '<div>' ).append(
171 $( '<p>' ).text( mw
.msg( 'upload-form-label-own-work' ) ),
175 new OO
.ui
.FieldLayout( this.messageLabel
, {
179 this.uploadForm
= new OO
.ui
.FormLayout( { items
: [ fieldset
] } );
182 this.selectFileWidget
.on( 'change', this.onUploadFormChange
.bind( this ) );
183 this.ownWorkCheckbox
.on( 'change', this.onUploadFormChange
.bind( this ) );
185 this.selectFileWidget
.on( 'change', function () {
186 var file
= layout
.getFile();
188 // Set the date to lastModified once we have the file
189 if ( layout
.getDateFromLastModified( file
) !== undefined ) {
190 layout
.dateWidget
.setValue( layout
.getDateFromLastModified( file
) );
193 // Check if we have EXIF data and set to that where available
194 layout
.getDateFromExif( file
).done( function ( date
) {
195 layout
.dateWidget
.setValue( date
);
198 layout
.updateFilePreview();
201 return this.uploadForm
;
207 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.onUploadFormChange = function () {
208 var file
= this.selectFileWidget
.getValue(),
209 ownWork
= this.ownWorkCheckbox
.isSelected(),
210 valid
= !!file
&& ownWork
;
211 this.emit( 'uploadValid', valid
);
217 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.renderInfoForm = function () {
220 this.filePreview
= new OO
.ui
.Widget( {
221 classes
: [ 'mw-upload-bookletLayout-filePreview' ]
223 this.progressBarWidget
= new OO
.ui
.ProgressBarWidget( {
226 this.filePreview
.$element
.append( this.progressBarWidget
.$element
);
228 this.filenameWidget
= new OO
.ui
.TextInputWidget( {
232 this.descriptionWidget
= new OO
.ui
.TextInputWidget( {
238 this.categoriesWidget
= new mw
.widgets
.CategorySelector( {
239 // Can't be done here because we don't know the target wiki yet... done in #initialize.
240 // api: new mw.ForeignApi( ... ),
241 $overlay
: this.$overlay
243 this.dateWidget
= new mw
.widgets
.DateInputWidget( {
244 $overlay
: this.$overlay
,
246 mustBeBefore
: moment().add( 1, 'day' ).locale( 'en' ).format( 'YYYY-MM-DD' ) // Tomorrow
249 this.filenameField
= new OO
.ui
.FieldLayout( this.filenameWidget
, {
250 label
: mw
.msg( 'upload-form-label-infoform-name' ),
252 classes
: [ 'mw-foreignStructuredUploa-bookletLayout-small-notice' ],
253 notices
: [ mw
.msg( 'upload-form-label-infoform-name-tooltip' ) ]
255 this.descriptionField
= new OO
.ui
.FieldLayout( this.descriptionWidget
, {
256 label
: mw
.msg( 'upload-form-label-infoform-description' ),
258 classes
: [ 'mw-foreignStructuredUploa-bookletLayout-small-notice' ],
259 notices
: [ mw
.msg( 'upload-form-label-infoform-description-tooltip' ) ]
261 this.categoriesField
= new OO
.ui
.FieldLayout( this.categoriesWidget
, {
262 label
: mw
.msg( 'upload-form-label-infoform-categories' ),
265 this.dateField
= new OO
.ui
.FieldLayout( this.dateWidget
, {
266 label
: mw
.msg( 'upload-form-label-infoform-date' ),
270 fieldset
= new OO
.ui
.FieldsetLayout( {
271 label
: mw
.msg( 'upload-form-label-infoform-title' )
275 this.descriptionField
,
276 this.categoriesField
,
279 this.infoForm
= new OO
.ui
.FormLayout( {
280 classes
: [ 'mw-upload-bookletLayout-infoForm' ],
281 items
: [ this.filePreview
, fieldset
]
285 this.filenameWidget
.on( 'change', this.onInfoFormChange
.bind( this ) );
286 this.descriptionWidget
.on( 'change', this.onInfoFormChange
.bind( this ) );
287 this.dateWidget
.on( 'change', this.onInfoFormChange
.bind( this ) );
289 this.on( 'fileUploadProgress', function ( progress
) {
290 this.progressBarWidget
.setProgress( progress
* 100 );
293 return this.infoForm
;
299 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.onInfoFormChange = function () {
301 validityPromises
= [];
303 validityPromises
.push( this.filenameWidget
.getValidity() );
304 if ( this.descriptionField
.isVisible() ) {
305 validityPromises
.push( this.descriptionWidget
.getValidity() );
307 if ( this.dateField
.isVisible() ) {
308 validityPromises
.push( this.dateWidget
.getValidity() );
311 $.when
.apply( $, validityPromises
).done( function () {
312 layout
.emit( 'infoValid', true );
313 } ).fail( function () {
314 layout
.emit( 'infoValid', false );
319 * @param {mw.Title} filename
320 * @return {jQuery.Promise} Resolves (on success) or rejects with OO.ui.Error
322 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.validateFilename = function ( filename
) {
323 return ( new mw
.Api() ).get( {
326 titles
: filename
.getPrefixedDb(),
329 function ( result
) {
330 // if the file already exists, reject right away, before
331 // ever firing finishStashUpload()
332 if ( !result
.query
.pages
[ 0 ].missing
) {
333 return $.Deferred().reject( new OO
.ui
.Error(
334 $( '<p>' ).msg( 'fileexists', filename
.getPrefixedDb() ),
335 { recoverable
: false }
340 // API call failed - this could be a connection hiccup...
341 // Let's just ignore this validation step and turn this
342 // failure into a successful resolve ;)
343 return $.Deferred().resolve();
351 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.saveFile = function () {
352 var title
= mw
.Title
.newFromText(
354 mw
.config
.get( 'wgNamespaceIds' ).file
357 return this.uploadPromise
358 .then( this.validateFilename
.bind( this, title
) )
359 .then( mw
.ForeignStructuredUpload
.BookletLayout
.parent
.prototype.saveFile
.bind( this ) );
367 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.getText = function () {
368 var language
= mw
.config
.get( 'wgContentLanguage' );
369 this.upload
.clearDescriptions();
370 this.upload
.addDescription( language
, this.descriptionWidget
.getValue() );
371 this.upload
.setDate( this.dateWidget
.getValue() );
372 this.upload
.clearCategories();
373 this.upload
.addCategories( this.categoriesWidget
.getItemsData() );
374 return this.upload
.getText();
378 * Get original date from EXIF data
380 * @param {Object} file
381 * @return {jQuery.Promise} Promise resolved with the EXIF date
383 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.getDateFromExif = function ( file
) {
385 deferred
= $.Deferred();
387 if ( file
&& file
.type
=== 'image/jpeg' ) {
388 fileReader
= new FileReader();
389 fileReader
.onload = function () {
390 var fileStr
, arr
, i
, metadata
;
392 if ( typeof fileReader
.result
=== 'string' ) {
393 fileStr
= fileReader
.result
;
395 // Array buffer; convert to binary string for the library.
396 arr
= new Uint8Array( fileReader
.result
);
398 for ( i
= 0; i
< arr
.byteLength
; i
++ ) {
399 fileStr
+= String
.fromCharCode( arr
[ i
] );
404 metadata
= mw
.libs
.jpegmeta( fileStr
, file
.name
);
409 if ( metadata
!== null && metadata
.exif
!== undefined && metadata
.exif
.DateTimeOriginal
) {
410 deferred
.resolve( moment( metadata
.exif
.DateTimeOriginal
, 'YYYY:MM:DD' ).format( 'YYYY-MM-DD' ) );
416 if ( 'readAsBinaryString' in fileReader
) {
417 fileReader
.readAsBinaryString( file
);
418 } else if ( 'readAsArrayBuffer' in fileReader
) {
419 fileReader
.readAsArrayBuffer( file
);
421 // We should never get here
423 throw new Error( 'Cannot read thumbnail as binary string or array buffer.' );
427 return deferred
.promise();
431 * Get last modified date from file
433 * @param {Object} file
434 * @return {Object} Last modified date from file
436 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.getDateFromLastModified = function ( file
) {
437 if ( file
&& file
.lastModified
) {
438 return moment( file
.lastModified
).format( 'YYYY-MM-DD' );
447 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.clear = function () {
448 mw
.ForeignStructuredUpload
.BookletLayout
.parent
.prototype.clear
.call( this );
450 this.ownWorkCheckbox
.setSelected( false );
451 this.categoriesWidget
.setItemsFromData( [] );
452 this.dateWidget
.setValue( '' ).setValidityFlag( true );
455 }( jQuery
, mediaWiki
) );