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
21 * @cfg {string} [target] Used to choose the target repository.
22 * If nothing is passed, the {@link mw.ForeignUpload#property-target default} is used.
24 mw
.ForeignStructuredUpload
.BookletLayout = function ( config
) {
25 config
= config
|| {};
27 mw
.ForeignStructuredUpload
.BookletLayout
.parent
.call( this, config
);
29 this.target
= config
.target
;
34 OO
.inheritClass( mw
.ForeignStructuredUpload
.BookletLayout
, mw
.Upload
.BookletLayout
);
41 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.initialize = function () {
43 return mw
.ForeignStructuredUpload
.BookletLayout
.parent
.prototype.initialize
.call( this ).then(
45 // Point the CategorySelector to the right wiki
46 return booklet
.upload
.getApi().then(
48 // If this is a ForeignApi, it will have a apiUrl, otherwise we don't need to do anything
50 // Can't reuse the same object, CategorySelector calls #abort on its mw.Api instance
51 booklet
.categoriesWidget
.api
= new mw
.ForeignApi( api
.apiUrl
);
53 return $.Deferred().resolve();
56 return $.Deferred().resolve();
61 return $.Deferred().resolve();
67 * Returns a {@link mw.ForeignStructuredUpload mw.ForeignStructuredUpload}
68 * with the {@link #cfg-target target} specified in config.
73 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.createUpload = function () {
74 return new mw
.ForeignStructuredUpload( this.target
);
82 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.renderUploadForm = function () {
84 query
= /[?&]uploadbucket=(\d)/.exec( location
.search
),
85 isTestEnabled
= !!mw
.config
.get( 'wgForeignUploadTestEnabled' ),
86 defaultBucket
= mw
.config
.get( 'wgForeignUploadTestDefault' ) || 1,
87 userId
= mw
.config
.get( 'wgUserId' );
89 if ( query
&& query
[ 1 ] ) {
90 // Testing and debugging
91 this.shouldRecordBucket
= false;
92 this.bucket
= Number( query
[ 1 ] );
93 } else if ( !userId
|| !isTestEnabled
) {
94 // a) Anonymous user. This can actually happen, because our software sucks.
95 // b) Test is not enabled on this wiki.
96 // In either case, display the old interface and don't record bucket on uploads.
97 this.shouldRecordBucket
= false;
98 this.bucket
= defaultBucket
;
100 // Regular logged in user on a wiki where the test is running
101 this.shouldRecordBucket
= true;
102 this.bucket
= ( userId
% 4 ) + 1; // 1, 2, 3, 4
105 return this[ 'renderUploadForm' + this.bucket
]();
109 * Test option 1, the original one. See T120867.
111 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.renderUploadForm1 = function () {
112 var fieldset
, $ownWorkMessage
, $notOwnWorkMessage
,
114 ownWorkMessage
, notOwnWorkMessage
, notOwnWorkLocal
,
115 validTargets
= mw
.config
.get( 'wgForeignUploadTargets' ),
116 target
= this.target
|| validTargets
[ 0 ] || 'local',
119 // Temporary override to make my life easier during A/B test
122 // foreign-structured-upload-form-label-own-work-message-local
123 // foreign-structured-upload-form-label-own-work-message-shared
124 ownWorkMessage
= mw
.message( 'foreign-structured-upload-form-label-own-work-message-' + target
);
125 // foreign-structured-upload-form-label-not-own-work-message-local
126 // foreign-structured-upload-form-label-not-own-work-message-shared
127 notOwnWorkMessage
= mw
.message( 'foreign-structured-upload-form-label-not-own-work-message-' + target
);
128 // foreign-structured-upload-form-label-not-own-work-local-local
129 // foreign-structured-upload-form-label-not-own-work-local-shared
130 notOwnWorkLocal
= mw
.message( 'foreign-structured-upload-form-label-not-own-work-local-' + target
);
132 if ( !ownWorkMessage
.exists() ) {
133 ownWorkMessage
= mw
.message( 'foreign-structured-upload-form-label-own-work-message-default' );
135 if ( !notOwnWorkMessage
.exists() ) {
136 notOwnWorkMessage
= mw
.message( 'foreign-structured-upload-form-label-not-own-work-message-default' );
138 if ( !notOwnWorkLocal
.exists() ) {
139 notOwnWorkLocal
= mw
.message( 'foreign-structured-upload-form-label-not-own-work-local-default' );
142 $ownWorkMessage
= $( '<p>' ).append( ownWorkMessage
.parseDom() )
143 .addClass( 'mw-foreignStructuredUpload-bookletLayout-license' );
144 $notOwnWorkMessage
= $( '<div>' ).append(
145 $( '<p>' ).append( notOwnWorkMessage
.parseDom() ),
146 $( '<p>' ).append( notOwnWorkLocal
.parseDom() )
148 $ownWorkMessage
.add( $notOwnWorkMessage
).find( 'a' )
149 .attr( 'target', '_blank' )
150 .on( 'click', function ( e
) {
151 // Some stupid code is trying to prevent default on all clicks, which causes the links to
152 // not be openable, don't let it
156 this.selectFileWidget
= new OO
.ui
.SelectFileWidget();
157 this.messageLabel
= new OO
.ui
.LabelWidget( {
158 label
: $notOwnWorkMessage
160 this.ownWorkCheckbox
= new OO
.ui
.CheckboxInputWidget().on( 'change', function ( on
) {
161 layout
.messageLabel
.toggle( !on
);
164 fieldset
= new OO
.ui
.FieldsetLayout();
166 new OO
.ui
.FieldLayout( this.selectFileWidget
, {
168 label
: mw
.msg( 'upload-form-label-select-file' )
170 new OO
.ui
.FieldLayout( this.ownWorkCheckbox
, {
172 label
: $( '<div>' ).append(
173 $( '<p>' ).text( mw
.msg( 'foreign-structured-upload-form-label-own-work' ) ),
177 new OO
.ui
.FieldLayout( this.messageLabel
, {
181 this.uploadForm
= new OO
.ui
.FormLayout( { items
: [ fieldset
] } );
183 onUploadFormChange = function () {
184 var file
= this.selectFileWidget
.getValue(),
185 ownWork
= this.ownWorkCheckbox
.isSelected(),
186 valid
= !!file
&& ownWork
;
187 this.emit( 'uploadValid', valid
);
191 this.selectFileWidget
.on( 'change', onUploadFormChange
.bind( this ) );
192 this.ownWorkCheckbox
.on( 'change', onUploadFormChange
.bind( this ) );
194 this.selectFileWidget
.on( 'change', function () {
195 var file
= layout
.getFile();
197 // Set the date to lastModified once we have the file
198 if ( layout
.getDateFromLastModified( file
) !== undefined ) {
199 layout
.dateWidget
.setValue( layout
.getDateFromLastModified( file
) );
202 // Check if we have EXIF data and set to that where available
203 layout
.getDateFromExif( file
).done( function ( date
) {
204 layout
.dateWidget
.setValue( date
);
208 return this.uploadForm
;
212 * Test option 2, idea A from T121021. See T120867.
214 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.renderUploadForm2 = function () {
215 var fieldset
, checkboxes
, fields
, onUploadFormChange
;
217 this.selectFileWidget
= new OO
.ui
.SelectFileWidget();
218 this.licenseCheckboxes
= checkboxes
= [
219 new OO
.ui
.CheckboxInputWidget(),
220 new OO
.ui
.CheckboxInputWidget(),
221 new OO
.ui
.CheckboxInputWidget(),
222 new OO
.ui
.CheckboxInputWidget()
226 new OO
.ui
.FieldLayout( this.selectFileWidget
, {
228 label
: mw
.msg( 'upload-form-label-select-file' )
230 new OO
.ui
.FieldLayout( new OO
.ui
.LabelWidget( {
231 label
: mw
.message( 'foreign-structured-upload-form-2-label-intro' ).parseDom()
235 new OO
.ui
.FieldLayout( checkboxes
[ 0 ], {
238 'mw-foreignStructuredUpload-bookletLayout-withicon',
239 'mw-foreignStructuredUpload-bookletLayout-ownwork'
241 label
: mw
.message( 'foreign-structured-upload-form-2-label-ownwork' ).parseDom()
243 new OO
.ui
.FieldLayout( checkboxes
[ 1 ], {
246 'mw-foreignStructuredUpload-bookletLayout-withicon',
247 'mw-foreignStructuredUpload-bookletLayout-noderiv'
249 label
: mw
.message( 'foreign-structured-upload-form-2-label-noderiv' ).parseDom()
251 new OO
.ui
.FieldLayout( checkboxes
[ 2 ], {
254 'mw-foreignStructuredUpload-bookletLayout-withicon',
255 'mw-foreignStructuredUpload-bookletLayout-useful'
257 label
: mw
.message( 'foreign-structured-upload-form-2-label-useful' ).parseDom()
259 new OO
.ui
.FieldLayout( checkboxes
[ 3 ], {
262 'mw-foreignStructuredUpload-bookletLayout-withicon',
263 'mw-foreignStructuredUpload-bookletLayout-ccbysa'
265 label
: mw
.message( 'foreign-structured-upload-form-2-label-ccbysa' ).parseDom()
267 new OO
.ui
.FieldLayout( new OO
.ui
.LabelWidget( {
269 .add( $( '<p>' ).msg( 'foreign-structured-upload-form-2-label-alternative' ) )
270 .add( $( '<p>' ).msg( 'foreign-structured-upload-form-2-label-termsofuse' )
271 .addClass( 'mw-foreignStructuredUpload-bookletLayout-license' ) )
277 fieldset
= new OO
.ui
.FieldsetLayout( { items
: fields
} );
278 this.uploadForm
= new OO
.ui
.FormLayout( { items
: [ fieldset
] } );
280 this.uploadForm
.$element
.find( 'a' )
281 .attr( 'target', '_blank' )
282 .on( 'click', function ( e
) {
283 // Some stupid code is trying to prevent default on all clicks, which causes the links to
284 // not be openable, don't let it
288 onUploadFormChange = function () {
289 var file
= this.selectFileWidget
.getValue(),
290 checks
= checkboxes
.every( function ( checkbox
) {
291 return checkbox
.isSelected();
293 valid
= !!file
&& checks
;
294 this.emit( 'uploadValid', valid
);
298 this.selectFileWidget
.on( 'change', onUploadFormChange
.bind( this ) );
299 checkboxes
[ 0 ].on( 'change', onUploadFormChange
.bind( this ) );
300 checkboxes
[ 1 ].on( 'change', onUploadFormChange
.bind( this ) );
301 checkboxes
[ 2 ].on( 'change', onUploadFormChange
.bind( this ) );
302 checkboxes
[ 3 ].on( 'change', onUploadFormChange
.bind( this ) );
304 return this.uploadForm
;
308 * Test option 3, idea D from T121021. See T120867.
310 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.renderUploadForm3 = function () {
311 var ownWorkCheckbox
, fieldset
, yesMsg
, noMsg
, selects
, selectFields
,
312 alternativeField
, fields
, onUploadFormChange
;
314 this.selectFileWidget
= new OO
.ui
.SelectFileWidget();
315 this.ownWorkCheckbox
= ownWorkCheckbox
= new OO
.ui
.CheckboxInputWidget();
317 yesMsg
= mw
.message( 'foreign-structured-upload-form-3-label-yes' ).text();
318 noMsg
= mw
.message( 'foreign-structured-upload-form-3-label-no' ).text();
320 new OO
.ui
.RadioSelectWidget( {
322 new OO
.ui
.RadioOptionWidget( { data
: false, label
: yesMsg
} ),
323 new OO
.ui
.RadioOptionWidget( { data
: true, label
: noMsg
} )
326 new OO
.ui
.RadioSelectWidget( {
328 new OO
.ui
.RadioOptionWidget( { data
: true, label
: yesMsg
} ),
329 new OO
.ui
.RadioOptionWidget( { data
: false, label
: noMsg
} )
332 new OO
.ui
.RadioSelectWidget( {
334 new OO
.ui
.RadioOptionWidget( { data
: false, label
: yesMsg
} ),
335 new OO
.ui
.RadioOptionWidget( { data
: true, label
: noMsg
} )
340 this.licenseSelectFields
= selectFields
= [
341 new OO
.ui
.FieldLayout( selects
[ 0 ], {
343 classes
: [ 'mw-foreignStructuredUpload-bookletLayout-question' ],
344 label
: mw
.message( 'foreign-structured-upload-form-3-label-question-website' ).parseDom()
346 new OO
.ui
.FieldLayout( selects
[ 1 ], {
348 classes
: [ 'mw-foreignStructuredUpload-bookletLayout-question' ],
349 label
: mw
.message( 'foreign-structured-upload-form-3-label-question-ownwork' ).parseDom()
351 new OO
.ui
.FieldLayout( selects
[ 2 ], {
353 classes
: [ 'mw-foreignStructuredUpload-bookletLayout-question' ],
354 label
: mw
.message( 'foreign-structured-upload-form-3-label-question-noderiv' ).parseDom()
358 alternativeField
= new OO
.ui
.FieldLayout( new OO
.ui
.LabelWidget( {
359 label
: mw
.message( 'foreign-structured-upload-form-3-label-alternative' ).parseDom()
364 // Choosing the right answer to each question shows the next question.
365 // Switching to wrong answer hides all subsequent questions.
366 selects
.forEach( function ( select
, i
) {
367 select
.on( 'choose', function ( selectedOption
) {
368 var isRightAnswer
= !!selectedOption
.getData();
369 alternativeField
.toggle( !isRightAnswer
);
370 if ( i
+ 1 === selectFields
.length
) {
374 if ( isRightAnswer
) {
375 selectFields
[ i
+ 1 ].toggle( true );
377 selectFields
.slice( i
+ 1 ).forEach( function ( field
) {
378 field
.fieldWidget
.selectItem( null );
379 field
.toggle( false );
386 new OO
.ui
.FieldLayout( this.selectFileWidget
, {
388 label
: mw
.msg( 'upload-form-label-select-file' )
394 new OO
.ui
.FieldLayout( ownWorkCheckbox
, {
395 classes
: [ 'mw-foreignStructuredUpload-bookletLayout-checkbox' ],
397 label
: mw
.message( 'foreign-structured-upload-form-label-own-work-message-shared' ).parseDom()
401 // Must be done late, after it's been associated with the FieldLayout
402 ownWorkCheckbox
.setDisabled( true );
404 fieldset
= new OO
.ui
.FieldsetLayout( { items
: fields
} );
405 this.uploadForm
= new OO
.ui
.FormLayout( { items
: [ fieldset
] } );
407 this.uploadForm
.$element
.find( 'a' )
408 .attr( 'target', '_blank' )
409 .on( 'click', function ( e
) {
410 // Some stupid code is trying to prevent default on all clicks, which causes the links to
411 // not be openable, don't let it
415 onUploadFormChange = function () {
416 var file
= this.selectFileWidget
.getValue(),
417 checkbox
= ownWorkCheckbox
.isSelected(),
418 rightAnswers
= selects
.every( function ( select
) {
419 return select
.getSelectedItem() && !!select
.getSelectedItem().getData();
421 valid
= !!file
&& checkbox
&& rightAnswers
;
422 ownWorkCheckbox
.setDisabled( !rightAnswers
);
423 if ( !rightAnswers
) {
424 ownWorkCheckbox
.setSelected( false );
426 this.emit( 'uploadValid', valid
);
430 this.selectFileWidget
.on( 'change', onUploadFormChange
.bind( this ) );
431 this.ownWorkCheckbox
.on( 'change', onUploadFormChange
.bind( this ) );
432 selects
[ 0 ].on( 'choose', onUploadFormChange
.bind( this ) );
433 selects
[ 1 ].on( 'choose', onUploadFormChange
.bind( this ) );
434 selects
[ 2 ].on( 'choose', onUploadFormChange
.bind( this ) );
436 return this.uploadForm
;
440 * Test option 4, idea E from T121021. See T120867.
442 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.renderUploadForm4 = function () {
443 var fieldset
, $guide
;
444 this.renderUploadForm1();
445 fieldset
= this.uploadForm
.getItems()[ 0 ];
447 $guide
= mw
.template
.get( 'mediawiki.ForeignStructuredUpload.BookletLayout', 'guide.html' ).render();
448 $guide
.find( '.mw-foreignStructuredUpload-bookletLayout-guide-text-wrapper-good span' )
449 .msg( 'foreign-structured-upload-form-4-label-good' );
450 $guide
.find( '.mw-foreignStructuredUpload-bookletLayout-guide-text-wrapper-bad span' )
451 .msg( 'foreign-structured-upload-form-4-label-bad' );
453 // Note the index, we insert after the SelectFileWidget field
455 new OO
.ui
.FieldLayout( new OO
.ui
.Widget( {
462 // Hook for custom styles
463 fieldset
.getItems()[ 2 ].$element
.addClass( 'mw-foreignStructuredUpload-bookletLayout-guide-checkbox' );
465 // Streamline: remove mention of local Special:Upload
466 fieldset
.getItems()[ 3 ].$element
.find( 'p' ).last().remove();
468 return this.uploadForm
;
474 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.onUploadFormChange = function () {};
479 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.renderInfoForm = function () {
482 this.filenameWidget
= new OO
.ui
.TextInputWidget( {
486 this.descriptionWidget
= new OO
.ui
.TextInputWidget( {
492 this.categoriesWidget
= new mw
.widgets
.CategorySelector( {
493 // Can't be done here because we don't know the target wiki yet... done in #initialize.
494 // api: new mw.ForeignApi( ... ),
495 $overlay
: this.$overlay
497 this.dateWidget
= new mw
.widgets
.DateInputWidget( {
498 $overlay
: this.$overlay
,
500 mustBeBefore
: moment().add( 1, 'day' ).locale( 'en' ).format( 'YYYY-MM-DD' ) // Tomorrow
503 fieldset
= new OO
.ui
.FieldsetLayout( {
504 label
: mw
.msg( 'upload-form-label-infoform-title' )
507 new OO
.ui
.FieldLayout( this.filenameWidget
, {
508 label
: mw
.msg( 'upload-form-label-infoform-name' ),
510 help
: mw
.msg( 'upload-form-label-infoform-name-tooltip' )
512 new OO
.ui
.FieldLayout( this.descriptionWidget
, {
513 label
: mw
.msg( 'upload-form-label-infoform-description' ),
515 help
: mw
.msg( 'upload-form-label-infoform-description-tooltip' )
517 new OO
.ui
.FieldLayout( this.categoriesWidget
, {
518 label
: mw
.msg( 'foreign-structured-upload-form-label-infoform-categories' ),
521 new OO
.ui
.FieldLayout( this.dateWidget
, {
522 label
: mw
.msg( 'foreign-structured-upload-form-label-infoform-date' ),
526 this.infoForm
= new OO
.ui
.FormLayout( { items
: [ fieldset
] } );
529 this.filenameWidget
.on( 'change', this.onInfoFormChange
.bind( this ) );
530 this.descriptionWidget
.on( 'change', this.onInfoFormChange
.bind( this ) );
531 this.dateWidget
.on( 'change', this.onInfoFormChange
.bind( this ) );
533 return this.infoForm
;
539 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.onInfoFormChange = function () {
542 this.filenameWidget
.getValidity(),
543 this.descriptionWidget
.getValidity(),
544 this.dateWidget
.getValidity()
545 ).done( function () {
546 layout
.emit( 'infoValid', true );
547 } ).fail( function () {
548 layout
.emit( 'infoValid', false );
557 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.getText = function () {
558 var language
= mw
.config
.get( 'wgContentLanguage' );
559 this.upload
.clearDescriptions();
560 this.upload
.addDescription( language
, this.descriptionWidget
.getValue() );
561 this.upload
.setDate( this.dateWidget
.getValue() );
562 this.upload
.clearCategories();
563 this.upload
.addCategories( this.categoriesWidget
.getItemsData() );
564 return this.upload
.getText();
568 * Get original date from EXIF data
570 * @param {Object} file
571 * @return {jQuery.Promise} Promise resolved with the EXIF date
573 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.getDateFromExif = function ( file
) {
575 deferred
= $.Deferred();
577 if ( file
&& file
.type
=== 'image/jpeg' ) {
578 fileReader
= new FileReader();
579 fileReader
.onload = function () {
580 var fileStr
, arr
, i
, metadata
;
582 if ( typeof fileReader
.result
=== 'string' ) {
583 fileStr
= fileReader
.result
;
585 // Array buffer; convert to binary string for the library.
586 arr
= new Uint8Array( fileReader
.result
);
588 for ( i
= 0; i
< arr
.byteLength
; i
++ ) {
589 fileStr
+= String
.fromCharCode( arr
[ i
] );
594 metadata
= mw
.libs
.jpegmeta( this.result
, file
.name
);
599 if ( metadata
!== null && metadata
.exif
!== undefined && metadata
.exif
.DateTimeOriginal
) {
600 deferred
.resolve( moment( metadata
.exif
.DateTimeOriginal
, 'YYYY:MM:DD' ).format( 'YYYY-MM-DD' ) );
606 if ( 'readAsBinaryString' in fileReader
) {
607 fileReader
.readAsBinaryString( file
);
608 } else if ( 'readAsArrayBuffer' in fileReader
) {
609 fileReader
.readAsArrayBuffer( file
);
611 // We should never get here
613 throw new Error( 'Cannot read thumbnail as binary string or array buffer.' );
617 return deferred
.promise();
621 * Get last modified date from file
623 * @param {Object} file
624 * @return {Object} Last modified date from file
626 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.getDateFromLastModified = function ( file
) {
627 if ( file
&& file
.lastModified
) {
628 return moment( file
.lastModified
).format( 'YYYY-MM-DD' );
637 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.clear = function () {
638 mw
.ForeignStructuredUpload
.BookletLayout
.parent
.prototype.clear
.call( this );
640 if ( this.ownWorkCheckbox
) {
641 this.ownWorkCheckbox
.setSelected( false );
643 if ( this.licenseCheckboxes
) {
644 this.licenseCheckboxes
.forEach( function ( checkbox
) {
645 checkbox
.setSelected( false );
648 if ( this.licenseSelectFields
) {
649 this.licenseSelectFields
.forEach( function ( field
, i
) {
650 field
.fieldWidget
.selectItem( null );
652 field
.toggle( false );
657 this.categoriesWidget
.setItemsFromData( [] );
658 this.dateWidget
.setValue( '' ).setValidityFlag( true );
661 }( jQuery
, mediaWiki
) );