48060973bc6239a0f154aa8bc0d508ecd63ccc2c
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 () {
42 var deferred
= $.Deferred();
43 mw
.ForeignStructuredUpload
.BookletLayout
.parent
.prototype.initialize
.call( this )
45 // Point the CategorySelector to the right wiki
46 this.upload
.getApi().done( function ( api
) {
47 // If this is a ForeignApi, it will have a apiUrl, otherwise we don't need to do anything
49 // Can't reuse the same object, CategorySelector calls #abort on its mw.Api instance
50 this.categoriesWidget
.api
= new mw
.ForeignApi( api
.apiUrl
);
55 return deferred
.promise();
59 * Returns a {@link mw.ForeignStructuredUpload mw.ForeignStructuredUpload}
60 * with the {@link #cfg-target target} specified in config.
65 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.createUpload = function () {
66 return new mw
.ForeignStructuredUpload( this.target
);
74 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.renderUploadForm = function () {
76 query
= /[?&]uploadbucket=(\d)/.exec( location
.search
),
77 isTestEnabled
= !!mw
.config
.get( 'wgForeignUploadTestEnabled' ),
78 defaultBucket
= mw
.config
.get( 'wgForeignUploadTestDefault' ) || 1,
79 userId
= mw
.config
.get( 'wgUserId' );
81 if ( query
&& query
[ 1 ] ) {
82 // Testing and debugging
83 this.shouldRecordBucket
= false;
84 this.bucket
= Number( query
[ 1 ] );
85 } else if ( !userId
|| !isTestEnabled
) {
86 // a) Anonymous user. This can actually happen, because our software sucks.
87 // b) Test is not enabled on this wiki.
88 // In either case, display the old interface and don't record bucket on uploads.
89 this.shouldRecordBucket
= false;
90 this.bucket
= defaultBucket
;
92 // Regular logged in user on a wiki where the test is running
93 this.shouldRecordBucket
= true;
94 this.bucket
= ( userId
% 4 ) + 1; // 1, 2, 3, 4
97 return this[ 'renderUploadForm' + this.bucket
]();
101 * Test option 1, the original one. See T120867.
103 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.renderUploadForm1 = function () {
104 var fieldset
, $ownWorkMessage
, $notOwnWorkMessage
,
106 ownWorkMessage
, notOwnWorkMessage
, notOwnWorkLocal
,
107 validTargets
= mw
.config
.get( 'wgForeignUploadTargets' ),
108 target
= this.target
|| validTargets
[ 0 ] || 'local',
111 // Temporary override to make my life easier during A/B test
114 // foreign-structured-upload-form-label-own-work-message-local
115 // foreign-structured-upload-form-label-own-work-message-shared
116 ownWorkMessage
= mw
.message( 'foreign-structured-upload-form-label-own-work-message-' + target
);
117 // foreign-structured-upload-form-label-not-own-work-message-local
118 // foreign-structured-upload-form-label-not-own-work-message-shared
119 notOwnWorkMessage
= mw
.message( 'foreign-structured-upload-form-label-not-own-work-message-' + target
);
120 // foreign-structured-upload-form-label-not-own-work-local-local
121 // foreign-structured-upload-form-label-not-own-work-local-shared
122 notOwnWorkLocal
= mw
.message( 'foreign-structured-upload-form-label-not-own-work-local-' + target
);
124 if ( !ownWorkMessage
.exists() ) {
125 ownWorkMessage
= mw
.message( 'foreign-structured-upload-form-label-own-work-message-default' );
127 if ( !notOwnWorkMessage
.exists() ) {
128 notOwnWorkMessage
= mw
.message( 'foreign-structured-upload-form-label-not-own-work-message-default' );
130 if ( !notOwnWorkLocal
.exists() ) {
131 notOwnWorkLocal
= mw
.message( 'foreign-structured-upload-form-label-not-own-work-local-default' );
134 $ownWorkMessage
= $( '<p>' ).html( ownWorkMessage
.parse() )
135 .addClass( 'mw-foreignStructuredUpload-bookletLayout-license' );
136 $notOwnWorkMessage
= $( '<div>' ).append(
137 $( '<p>' ).html( notOwnWorkMessage
.parse() ),
138 $( '<p>' ).html( notOwnWorkLocal
.parse() )
140 $ownWorkMessage
.add( $notOwnWorkMessage
).find( 'a' )
141 .attr( 'target', '_blank' )
142 .on( 'click', function ( e
) {
143 // Some stupid code is trying to prevent default on all clicks, which causes the links to
144 // not be openable, don't let it
148 this.selectFileWidget
= new OO
.ui
.SelectFileWidget();
149 this.messageLabel
= new OO
.ui
.LabelWidget( {
150 label
: $notOwnWorkMessage
152 this.ownWorkCheckbox
= new OO
.ui
.CheckboxInputWidget().on( 'change', function ( on
) {
153 layout
.messageLabel
.toggle( !on
);
156 fieldset
= new OO
.ui
.FieldsetLayout();
158 new OO
.ui
.FieldLayout( this.selectFileWidget
, {
160 label
: mw
.msg( 'upload-form-label-select-file' )
162 new OO
.ui
.FieldLayout( this.ownWorkCheckbox
, {
164 label
: $( '<div>' ).append(
165 $( '<p>' ).text( mw
.msg( 'foreign-structured-upload-form-label-own-work' ) ),
169 new OO
.ui
.FieldLayout( this.messageLabel
, {
173 this.uploadForm
= new OO
.ui
.FormLayout( { items
: [ fieldset
] } );
175 onUploadFormChange = function () {
176 var file
= this.selectFileWidget
.getValue(),
177 ownWork
= this.ownWorkCheckbox
.isSelected(),
178 valid
= !!file
&& ownWork
;
179 this.emit( 'uploadValid', valid
);
183 this.selectFileWidget
.on( 'change', onUploadFormChange
.bind( this ) );
184 this.ownWorkCheckbox
.on( 'change', onUploadFormChange
.bind( this ) );
186 this.selectFileWidget
.on( 'change', function () {
187 var file
= layout
.getFile();
189 // Set the date to lastModified once we have the file
190 if ( layout
.getDateFromLastModified( file
) !== undefined ) {
191 layout
.dateWidget
.setValue( layout
.getDateFromLastModified( file
) );
194 // Check if we have EXIF data and set to that where available
195 layout
.getDateFromExif( file
).done( function ( date
) {
196 layout
.dateWidget
.setValue( date
);
200 return this.uploadForm
;
204 * Test option 2, idea A from T121021. See T120867.
206 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.renderUploadForm2 = function () {
207 var fieldset
, checkboxes
, fields
, onUploadFormChange
;
209 this.selectFileWidget
= new OO
.ui
.SelectFileWidget();
210 this.licenseCheckboxes
= checkboxes
= [
211 new OO
.ui
.CheckboxInputWidget(),
212 new OO
.ui
.CheckboxInputWidget(),
213 new OO
.ui
.CheckboxInputWidget(),
214 new OO
.ui
.CheckboxInputWidget()
218 new OO
.ui
.FieldLayout( this.selectFileWidget
, {
220 label
: mw
.msg( 'upload-form-label-select-file' )
222 new OO
.ui
.FieldLayout( new OO
.ui
.LabelWidget( {
223 label
: mw
.message( 'foreign-structured-upload-form-2-label-intro' ).parseDom()
227 new OO
.ui
.FieldLayout( checkboxes
[ 0 ], {
230 'mw-foreignStructuredUpload-bookletLayout-withicon',
231 'mw-foreignStructuredUpload-bookletLayout-ownwork'
233 label
: mw
.message( 'foreign-structured-upload-form-2-label-ownwork' ).parseDom()
235 new OO
.ui
.FieldLayout( checkboxes
[ 1 ], {
238 'mw-foreignStructuredUpload-bookletLayout-withicon',
239 'mw-foreignStructuredUpload-bookletLayout-noderiv'
241 label
: mw
.message( 'foreign-structured-upload-form-2-label-noderiv' ).parseDom()
243 new OO
.ui
.FieldLayout( checkboxes
[ 2 ], {
246 'mw-foreignStructuredUpload-bookletLayout-withicon',
247 'mw-foreignStructuredUpload-bookletLayout-useful'
249 label
: mw
.message( 'foreign-structured-upload-form-2-label-useful' ).parseDom()
251 new OO
.ui
.FieldLayout( checkboxes
[ 3 ], {
254 'mw-foreignStructuredUpload-bookletLayout-withicon',
255 'mw-foreignStructuredUpload-bookletLayout-ccbysa'
257 label
: mw
.message( 'foreign-structured-upload-form-2-label-ccbysa' ).parseDom()
259 new OO
.ui
.FieldLayout( new OO
.ui
.LabelWidget( {
261 .add( $( '<p>' ).msg( 'foreign-structured-upload-form-2-label-alternative' ) )
262 .add( $( '<p>' ).msg( 'foreign-structured-upload-form-2-label-termsofuse' )
263 .addClass( 'mw-foreignStructuredUpload-bookletLayout-license' ) )
269 fieldset
= new OO
.ui
.FieldsetLayout( { items
: fields
} );
270 this.uploadForm
= new OO
.ui
.FormLayout( { items
: [ fieldset
] } );
272 this.uploadForm
.$element
.find( 'a' )
273 .attr( 'target', '_blank' )
274 .on( 'click', function ( e
) {
275 // Some stupid code is trying to prevent default on all clicks, which causes the links to
276 // not be openable, don't let it
280 onUploadFormChange = function () {
281 var file
= this.selectFileWidget
.getValue(),
282 checks
= checkboxes
.every( function ( checkbox
) {
283 return checkbox
.isSelected();
285 valid
= !!file
&& checks
;
286 this.emit( 'uploadValid', valid
);
290 this.selectFileWidget
.on( 'change', onUploadFormChange
.bind( this ) );
291 checkboxes
[ 0 ].on( 'change', onUploadFormChange
.bind( this ) );
292 checkboxes
[ 1 ].on( 'change', onUploadFormChange
.bind( this ) );
293 checkboxes
[ 2 ].on( 'change', onUploadFormChange
.bind( this ) );
294 checkboxes
[ 3 ].on( 'change', onUploadFormChange
.bind( this ) );
296 return this.uploadForm
;
300 * Test option 3, idea D from T121021. See T120867.
302 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.renderUploadForm3 = function () {
303 var ownWorkCheckbox
, fieldset
, yesMsg
, noMsg
, selects
, selectFields
,
304 alternativeField
, fields
, onUploadFormChange
;
306 this.selectFileWidget
= new OO
.ui
.SelectFileWidget();
307 this.ownWorkCheckbox
= ownWorkCheckbox
= new OO
.ui
.CheckboxInputWidget();
309 yesMsg
= mw
.message( 'foreign-structured-upload-form-3-label-yes' ).text();
310 noMsg
= mw
.message( 'foreign-structured-upload-form-3-label-no' ).text();
312 new OO
.ui
.RadioSelectWidget( {
314 new OO
.ui
.RadioOptionWidget( { data
: false, label
: yesMsg
} ),
315 new OO
.ui
.RadioOptionWidget( { data
: true, label
: noMsg
} )
318 new OO
.ui
.RadioSelectWidget( {
320 new OO
.ui
.RadioOptionWidget( { data
: true, label
: yesMsg
} ),
321 new OO
.ui
.RadioOptionWidget( { data
: false, label
: noMsg
} )
324 new OO
.ui
.RadioSelectWidget( {
326 new OO
.ui
.RadioOptionWidget( { data
: false, label
: yesMsg
} ),
327 new OO
.ui
.RadioOptionWidget( { data
: true, label
: noMsg
} )
332 this.licenseSelectFields
= selectFields
= [
333 new OO
.ui
.FieldLayout( selects
[ 0 ], {
335 classes
: [ 'mw-foreignStructuredUpload-bookletLayout-question' ],
336 label
: mw
.message( 'foreign-structured-upload-form-3-label-question-website' ).parseDom()
338 new OO
.ui
.FieldLayout( selects
[ 1 ], {
340 classes
: [ 'mw-foreignStructuredUpload-bookletLayout-question' ],
341 label
: mw
.message( 'foreign-structured-upload-form-3-label-question-ownwork' ).parseDom()
343 new OO
.ui
.FieldLayout( selects
[ 2 ], {
345 classes
: [ 'mw-foreignStructuredUpload-bookletLayout-question' ],
346 label
: mw
.message( 'foreign-structured-upload-form-3-label-question-noderiv' ).parseDom()
350 alternativeField
= new OO
.ui
.FieldLayout( new OO
.ui
.LabelWidget( {
351 label
: mw
.message( 'foreign-structured-upload-form-3-label-alternative' ).parseDom()
356 // Choosing the right answer to each question shows the next question.
357 // Switching to wrong answer hides all subsequent questions.
358 selects
.forEach( function ( select
, i
) {
359 select
.on( 'choose', function ( selectedOption
) {
360 var isRightAnswer
= !!selectedOption
.getData();
361 alternativeField
.toggle( !isRightAnswer
);
362 if ( i
+ 1 === selectFields
.length
) {
366 if ( isRightAnswer
) {
367 selectFields
[ i
+ 1 ].toggle( true );
369 selectFields
.slice( i
+ 1 ).forEach( function ( field
) {
370 field
.fieldWidget
.selectItem( null );
371 field
.toggle( false );
378 new OO
.ui
.FieldLayout( this.selectFileWidget
, {
380 label
: mw
.msg( 'upload-form-label-select-file' )
386 new OO
.ui
.FieldLayout( ownWorkCheckbox
, {
387 classes
: [ 'mw-foreignStructuredUpload-bookletLayout-checkbox' ],
389 label
: mw
.message( 'foreign-structured-upload-form-label-own-work-message-shared' ).parseDom()
393 // Must be done late, after it's been associated with the FieldLayout
394 ownWorkCheckbox
.setDisabled( true );
396 fieldset
= new OO
.ui
.FieldsetLayout( { items
: fields
} );
397 this.uploadForm
= new OO
.ui
.FormLayout( { items
: [ fieldset
] } );
399 this.uploadForm
.$element
.find( 'a' )
400 .attr( 'target', '_blank' )
401 .on( 'click', function ( e
) {
402 // Some stupid code is trying to prevent default on all clicks, which causes the links to
403 // not be openable, don't let it
407 onUploadFormChange = function () {
408 var file
= this.selectFileWidget
.getValue(),
409 checkbox
= ownWorkCheckbox
.isSelected(),
410 rightAnswers
= selects
.every( function ( select
) {
411 return select
.getSelectedItem() && !!select
.getSelectedItem().getData();
413 valid
= !!file
&& checkbox
&& rightAnswers
;
414 ownWorkCheckbox
.setDisabled( !rightAnswers
);
415 if ( !rightAnswers
) {
416 ownWorkCheckbox
.setSelected( false );
418 this.emit( 'uploadValid', valid
);
422 this.selectFileWidget
.on( 'change', onUploadFormChange
.bind( this ) );
423 this.ownWorkCheckbox
.on( 'change', onUploadFormChange
.bind( this ) );
424 selects
[ 0 ].on( 'choose', onUploadFormChange
.bind( this ) );
425 selects
[ 1 ].on( 'choose', onUploadFormChange
.bind( this ) );
426 selects
[ 2 ].on( 'choose', onUploadFormChange
.bind( this ) );
428 return this.uploadForm
;
432 * Test option 4, idea E from T121021. See T120867.
434 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.renderUploadForm4 = function () {
435 var fieldset
, $guide
;
436 this.renderUploadForm1();
437 fieldset
= this.uploadForm
.getItems()[ 0 ];
439 $guide
= mw
.template
.get( 'mediawiki.ForeignStructuredUpload.BookletLayout', 'guide.html' ).render();
440 $guide
.find( '.mw-foreignStructuredUpload-bookletLayout-guide-text-wrapper-good span' )
441 .msg( 'foreign-structured-upload-form-4-label-good' );
442 $guide
.find( '.mw-foreignStructuredUpload-bookletLayout-guide-text-wrapper-bad span' )
443 .msg( 'foreign-structured-upload-form-4-label-bad' );
445 // Note the index, we insert after the SelectFileWidget field
447 new OO
.ui
.FieldLayout( new OO
.ui
.Widget( {
454 // Hook for custom styles
455 fieldset
.getItems()[ 2 ].$element
.addClass( 'mw-foreignStructuredUpload-bookletLayout-guide-checkbox' );
457 // Streamline: remove mention of local Special:Upload
458 fieldset
.getItems()[ 3 ].$element
.find( 'p' ).last().remove();
460 return this.uploadForm
;
466 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.onUploadFormChange = function () {};
471 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.renderInfoForm = function () {
474 this.filenameWidget
= new OO
.ui
.TextInputWidget( {
478 this.descriptionWidget
= new OO
.ui
.TextInputWidget( {
484 this.categoriesWidget
= new mw
.widgets
.CategorySelector( {
485 // Can't be done here because we don't know the target wiki yet... done in #initialize.
486 // api: new mw.ForeignApi( ... ),
487 $overlay
: this.$overlay
489 this.dateWidget
= new mw
.widgets
.DateInputWidget( {
490 $overlay
: this.$overlay
,
492 mustBeBefore
: moment().add( 1, 'day' ).locale( 'en' ).format( 'YYYY-MM-DD' ) // Tomorrow
495 fieldset
= new OO
.ui
.FieldsetLayout( {
496 label
: mw
.msg( 'upload-form-label-infoform-title' )
499 new OO
.ui
.FieldLayout( this.filenameWidget
, {
500 label
: mw
.msg( 'upload-form-label-infoform-name' ),
502 help
: mw
.msg( 'upload-form-label-infoform-name-tooltip' )
504 new OO
.ui
.FieldLayout( this.descriptionWidget
, {
505 label
: mw
.msg( 'upload-form-label-infoform-description' ),
507 help
: mw
.msg( 'upload-form-label-infoform-description-tooltip' )
509 new OO
.ui
.FieldLayout( this.categoriesWidget
, {
510 label
: mw
.msg( 'foreign-structured-upload-form-label-infoform-categories' ),
513 new OO
.ui
.FieldLayout( this.dateWidget
, {
514 label
: mw
.msg( 'foreign-structured-upload-form-label-infoform-date' ),
518 this.infoForm
= new OO
.ui
.FormLayout( { items
: [ fieldset
] } );
521 this.filenameWidget
.on( 'change', this.onInfoFormChange
.bind( this ) );
522 this.descriptionWidget
.on( 'change', this.onInfoFormChange
.bind( this ) );
523 this.dateWidget
.on( 'change', this.onInfoFormChange
.bind( this ) );
525 return this.infoForm
;
531 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.onInfoFormChange = function () {
534 this.filenameWidget
.getValidity(),
535 this.descriptionWidget
.getValidity(),
536 this.dateWidget
.getValidity()
537 ).done( function () {
538 layout
.emit( 'infoValid', true );
539 } ).fail( function () {
540 layout
.emit( 'infoValid', false );
549 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.getText = function () {
550 var language
= mw
.config
.get( 'wgContentLanguage' );
551 this.upload
.clearDescriptions();
552 this.upload
.addDescription( language
, this.descriptionWidget
.getValue() );
553 this.upload
.setDate( this.dateWidget
.getValue() );
554 this.upload
.clearCategories();
555 this.upload
.addCategories( this.categoriesWidget
.getItemsData() );
556 return this.upload
.getText();
560 * Get original date from EXIF data
562 * @param {Object} file
563 * @return {jQuery.Promise} Promise resolved with the EXIF date
565 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.getDateFromExif = function ( file
) {
567 deferred
= $.Deferred();
569 if ( file
&& file
.type
=== 'image/jpeg' ) {
570 fileReader
= new FileReader();
571 fileReader
.onload = function () {
572 var fileStr
, arr
, i
, metadata
;
574 if ( typeof fileReader
.result
=== 'string' ) {
575 fileStr
= fileReader
.result
;
577 // Array buffer; convert to binary string for the library.
578 arr
= new Uint8Array( fileReader
.result
);
580 for ( i
= 0; i
< arr
.byteLength
; i
++ ) {
581 fileStr
+= String
.fromCharCode( arr
[ i
] );
586 metadata
= mw
.libs
.jpegmeta( this.result
, file
.name
);
591 if ( metadata
!== null && metadata
.exif
!== undefined && metadata
.exif
.DateTimeOriginal
) {
592 deferred
.resolve( moment( metadata
.exif
.DateTimeOriginal
, 'YYYY:MM:DD' ).format( 'YYYY-MM-DD' ) );
598 if ( 'readAsBinaryString' in fileReader
) {
599 fileReader
.readAsBinaryString( file
);
600 } else if ( 'readAsArrayBuffer' in fileReader
) {
601 fileReader
.readAsArrayBuffer( file
);
603 // We should never get here
605 throw new Error( 'Cannot read thumbnail as binary string or array buffer.' );
609 return deferred
.promise();
613 * Get last modified date from file
615 * @param {Object} file
616 * @return {Object} Last modified date from file
618 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.getDateFromLastModified = function ( file
) {
619 if ( file
&& file
.lastModified
) {
620 return moment( file
.lastModified
).format( 'YYYY-MM-DD' );
629 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.clear = function () {
630 mw
.ForeignStructuredUpload
.BookletLayout
.parent
.prototype.clear
.call( this );
632 if ( this.ownWorkCheckbox
) {
633 this.ownWorkCheckbox
.setSelected( false );
635 if ( this.licenseCheckboxes
) {
636 this.licenseCheckboxes
.forEach( function ( checkbox
) {
637 checkbox
.setSelected( false );
640 if ( this.licenseSelectFields
) {
641 this.licenseSelectFields
.forEach( function ( field
, i
) {
642 field
.fieldWidget
.selectItem( null );
644 field
.toggle( false );
649 this.categoriesWidget
.setItemsFromData( [] );
650 this.dateWidget
.setValue( '' ).setValidityFlag( true );
653 }( jQuery
, mediaWiki
) );