5 * @classdesc Encapsulates the process of uploading a file to MediaWiki
6 * using the {@link mw.ForeignStructuredUpload} model.
9 * var uploadDialog = new mw.Upload.Dialog( {
10 * bookletClass: mw.ForeignStructuredUpload.BookletLayout,
15 * var windowManager = new OO.ui.WindowManager();
16 * $( document.body ).append( windowManager.$element );
17 * windowManager.addWindows( [ uploadDialog ] );
19 * @class mw.ForeignStructuredUpload.BookletLayout
20 * @extends mw.Upload.BookletLayout
23 * @description Create an instance of `mw.ForeignStructuredUpload.BookletLayout`.
24 * @param {Object} config Configuration options
25 * @param {string} [config.target] Used to choose the target repository.
26 * If nothing is passed, the {@link mw.ForeignUpload#property-target default} is used.
28 mw
.ForeignStructuredUpload
.BookletLayout = function ( config
) {
29 config
= config
|| {};
31 mw
.ForeignStructuredUpload
.BookletLayout
.super.call( this, config
);
33 this.target
= config
.target
;
38 OO
.inheritClass( mw
.ForeignStructuredUpload
.BookletLayout
, mw
.Upload
.BookletLayout
);
46 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.initialize = function () {
47 return mw
.ForeignStructuredUpload
.BookletLayout
.super.prototype.initialize
.call( this ).then(
49 // Point the CategoryMultiselectWidget to the right wiki
50 this.upload
.getApi().then( ( 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, CategoryMultiselectWidget calls #abort on its mw.Api instance
54 this.categoriesWidget
.api
= new mw
.ForeignApi( api
.apiUrl
);
56 return $.Deferred().resolve();
58 // Set up booklet fields and license messages to match configuration
59 this.upload
.loadConfig().then( ( config
) => {
60 const isLocal
= this.upload
.target
=== 'local',
61 fields
= config
.fields
,
62 msgs
= config
.licensemessages
[ isLocal
? 'local' : 'foreign' ];
64 // Hide disabled fields
65 this.descriptionField
.toggle( !!fields
.description
);
66 this.categoriesField
.toggle( !!fields
.categories
);
67 this.dateField
.toggle( !!fields
.date
);
68 // Update form validity
69 this.onInfoFormChange();
72 // Load license messages from the remote wiki if we don't have these messages locally
73 // (this means that we only load messages from the foreign wiki for custom config)
74 // These messages are documented where msgPromise resolves
75 if ( mw
.message( 'upload-form-label-own-work-message-' + msgs
).exists() ) {
76 msgPromise
= $.Deferred().resolve();
78 msgPromise
= this.upload
.apiPromise
.then( ( api
) => api
.loadMessages( [
79 // These messages are documented where msgPromise resolves
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
86 // Update license messages
87 return msgPromise
.then( () => {
88 // The following messages are used here:
89 // * upload-form-label-own-work-message-generic-local
90 // * upload-form-label-own-work-message-generic-foreign
91 this.$ownWorkMessage
.msg( 'upload-form-label-own-work-message-' + msgs
);
92 // * upload-form-label-not-own-work-message-generic-local
93 // * upload-form-label-not-own-work-message-generic-foreign
94 this.$notOwnWorkMessage
.msg( 'upload-form-label-not-own-work-message-' + msgs
);
95 // * upload-form-label-not-own-work-local-generic-local
96 // * upload-form-label-not-own-work-local-generic-foreign
97 this.$notOwnWorkLocal
.msg( 'upload-form-label-not-own-work-local-' + msgs
);
100 this.$ownWorkMessage
[ 0 ],
101 this.$notOwnWorkMessage
[ 0 ],
102 this.$notOwnWorkLocal
[ 0 ]
105 // Improve the behavior of links inside these labels, which may point to important
106 // things like licensing requirements or terms of use
108 .attr( 'target', '_blank' )
109 .on( 'click', ( e
) => {
110 // OO.ui.FieldLayout#onLabelClick is trying to prevent default on all clicks,
111 // which causes the links to not be openable. Don't let it do that.
116 // eslint-disable-next-line mediawiki/msg-doc
117 this.getPage( 'upload' ).$element
.msg( errorMsg
);
118 return $.Deferred().resolve();
122 // Always resolve, never reject
123 () => $.Deferred().resolve()
128 * Returns a {@link mw.ForeignStructuredUpload mw.ForeignStructuredUpload}
129 * with the `target` specified in config.
132 * @return {mw.Upload}
134 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.createUpload = function () {
135 return new mw
.ForeignStructuredUpload( this.target
, {
138 errorlang
: mw
.config
.get( 'wgUserLanguage' ),
150 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.renderUploadForm = function () {
151 // These elements are filled with text in #initialize
152 // TODO Refactor this to be in one place
153 this.$ownWorkMessage
= $( '<p>' );
154 this.$notOwnWorkMessage
= $( '<p>' );
155 this.$notOwnWorkLocal
= $( '<p>' );
157 this.selectFileWidget
= new OO
.ui
.SelectFileInputWidget( {
160 this.messageLabel
= new OO
.ui
.LabelWidget( {
161 label
: $( '<div>' ).append(
162 this.$notOwnWorkMessage
,
163 this.$notOwnWorkLocal
166 this.ownWorkCheckbox
= new OO
.ui
.CheckboxInputWidget().on( 'change', ( on
) => {
167 this.messageLabel
.toggle( !on
);
170 const fieldset
= new OO
.ui
.FieldsetLayout();
172 new OO
.ui
.FieldLayout( this.selectFileWidget
, {
175 new OO
.ui
.FieldLayout( this.ownWorkCheckbox
, {
177 label
: mw
.msg( 'upload-form-label-own-work' ),
178 help
: this.$ownWorkMessage
,
181 new OO
.ui
.FieldLayout( this.messageLabel
, {
185 this.uploadForm
= new OO
.ui
.FormLayout( { items
: [ fieldset
] } );
188 this.selectFileWidget
.on( 'change', this.onUploadFormChange
.bind( this ) );
189 this.ownWorkCheckbox
.on( 'change', this.onUploadFormChange
.bind( this ) );
191 this.selectFileWidget
.on( 'change', () => {
192 const file
= this.getFile();
194 // Set the date to lastModified once we have the file
195 if ( this.getDateFromLastModified( file
) !== undefined ) {
196 this.dateWidget
.setValue( this.getDateFromLastModified( file
) );
199 // Check if we have EXIF data and set to that where available
200 this.getDateFromExif( file
).done( ( date
) => {
201 this.dateWidget
.setValue( date
);
204 this.updateFilePreview();
207 return this.uploadForm
;
213 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.onUploadFormChange = function () {
214 const file
= this.selectFileWidget
.getValue(),
215 ownWork
= this.ownWorkCheckbox
.isSelected(),
216 valid
= !!file
&& ownWork
;
217 this.emit( 'uploadValid', valid
);
223 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.renderInfoForm = function () {
224 this.filePreview
= new OO
.ui
.Widget( {
225 classes
: [ 'mw-upload-bookletLayout-filePreview' ]
227 this.progressBarWidget
= new OO
.ui
.ProgressBarWidget( {
230 this.filePreview
.$element
.append( this.progressBarWidget
.$element
);
232 this.filenameWidget
= new OO
.ui
.TextInputWidget( {
236 this.descriptionWidget
= new OO
.ui
.MultilineTextInputWidget( {
241 this.categoriesWidget
= new mw
.widgets
.CategoryMultiselectWidget( {
242 // Can't be done here because we don't know the target wiki yet... done in #initialize.
243 // api: new mw.ForeignApi( ... ),
244 $overlay
: this.$overlay
246 this.dateWidget
= new mw
.widgets
.DateInputWidget( {
247 $overlay
: this.$overlay
,
249 mustBeBefore
: moment().add( 1, 'day' ).locale( 'en' ).format( 'YYYY-MM-DD' ) // Tomorrow
252 this.filenameField
= new OO
.ui
.FieldLayout( this.filenameWidget
, {
253 label
: mw
.msg( 'upload-form-label-infoform-name' ),
255 help
: mw
.msg( 'upload-form-label-infoform-name-tooltip' ),
258 this.descriptionField
= new OO
.ui
.FieldLayout( this.descriptionWidget
, {
259 label
: mw
.msg( 'upload-form-label-infoform-description' ),
261 help
: mw
.msg( 'upload-form-label-infoform-description-tooltip' ),
264 this.categoriesField
= new OO
.ui
.FieldLayout( this.categoriesWidget
, {
265 label
: mw
.msg( 'upload-form-label-infoform-categories' ),
268 this.dateField
= new OO
.ui
.FieldLayout( this.dateWidget
, {
269 label
: mw
.msg( 'upload-form-label-infoform-date' ),
273 const fieldset
= new OO
.ui
.FieldsetLayout( {
274 label
: mw
.msg( 'upload-form-label-infoform-title' )
278 this.descriptionField
,
279 this.categoriesField
,
282 this.infoForm
= new OO
.ui
.FormLayout( {
283 classes
: [ 'mw-upload-bookletLayout-infoForm' ],
284 items
: [ this.filePreview
, fieldset
]
288 this.filenameWidget
.on( 'change', this.onInfoFormChange
.bind( this ) );
289 this.descriptionWidget
.on( 'change', this.onInfoFormChange
.bind( this ) );
290 this.dateWidget
.on( 'change', this.onInfoFormChange
.bind( this ) );
292 this.on( 'fileUploadProgress', ( progress
) => {
293 this.progressBarWidget
.setProgress( progress
* 100 );
296 return this.infoForm
;
302 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.onInfoFormChange = function () {
303 const validityPromises
= [];
305 validityPromises
.push( this.filenameWidget
.getValidity() );
306 if ( this.descriptionField
.isVisible() ) {
307 validityPromises
.push( this.descriptionWidget
.getValidity() );
309 if ( this.dateField
.isVisible() ) {
310 validityPromises
.push( this.dateWidget
.getValidity() );
313 $.when( ...validityPromises
).done( () => {
314 this.emit( 'infoValid', true );
316 this.emit( 'infoValid', false );
321 * @param {mw.Title} filename
322 * @return {jQuery.Promise} Resolves (on success) or rejects with OO.ui.Error
324 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.validateFilename = function ( filename
) {
325 return ( new mw
.Api() ).get( {
328 titles
: filename
.getPrefixedDb(),
332 // if the file already exists, reject right away, before
333 // ever firing finishStashUpload()
334 if ( !result
.query
.pages
[ 0 ].missing
) {
335 return $.Deferred().reject( new OO
.ui
.Error(
336 $( '<p>' ).msg( 'fileexists', filename
.getPrefixedDb() ),
337 { recoverable
: false }
341 // API call failed - this could be a connection hiccup...
342 // Let's just ignore this validation step and turn this
343 // failure into a successful resolve ;)
344 () => $.Deferred().resolve()
351 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.saveFile = function () {
352 const 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
.super.prototype.saveFile
.bind( this ) );
367 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.getText = function () {
368 const language
= mw
.config
.get( 'wgContentLanguage' ),
369 categories
= this.categoriesWidget
.getItems().map( ( item
) => item
.data
);
370 this.upload
.clearDescriptions();
371 this.upload
.addDescription( language
, this.descriptionWidget
.getValue() );
372 this.upload
.setDate( this.dateWidget
.getValue() );
373 this.upload
.clearCategories();
374 this.upload
.addCategories( categories
);
375 return this.upload
.getText();
379 * Get original date from EXIF data.
382 * @return {jQuery.Promise} Promise resolved with the EXIF date
384 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.getDateFromExif = function ( file
) {
385 const deferred
= $.Deferred();
387 if ( file
&& file
.type
=== 'image/jpeg' ) {
388 const fileReader
= new FileReader();
389 fileReader
.onload = function () {
390 const jpegmeta
= require( 'mediawiki.libs.jpegmeta' );
393 if ( typeof fileReader
.result
=== 'string' ) {
394 fileStr
= fileReader
.result
;
396 // Array buffer; convert to binary string for the library.
397 const arr
= new Uint8Array( fileReader
.result
);
399 for ( let i
= 0; i
< arr
.byteLength
; i
++ ) {
400 fileStr
+= String
.fromCharCode( arr
[ i
] );
406 metadata
= jpegmeta( fileStr
, file
.name
);
411 if ( metadata
!== null && metadata
.exif
!== undefined && metadata
.exif
.DateTimeOriginal
) {
412 deferred
.resolve( moment( metadata
.exif
.DateTimeOriginal
, 'YYYY:MM:DD' ).format( 'YYYY-MM-DD' ) );
418 if ( 'readAsBinaryString' in fileReader
) {
419 fileReader
.readAsBinaryString( file
);
420 } else if ( 'readAsArrayBuffer' in fileReader
) {
421 fileReader
.readAsArrayBuffer( file
);
423 // We should never get here
425 throw new Error( 'Cannot read thumbnail as binary string or array buffer.' );
429 return deferred
.promise();
433 * Get last modified date from file.
436 * @return {string|undefined} Last modified date from file
438 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.getDateFromLastModified = function ( file
) {
439 if ( file
&& file
.lastModified
) {
440 return moment( file
.lastModified
).format( 'YYYY-MM-DD' );
449 mw
.ForeignStructuredUpload
.BookletLayout
.prototype.clear = function () {
450 mw
.ForeignStructuredUpload
.BookletLayout
.super.prototype.clear
.call( this );
452 this.ownWorkCheckbox
.setSelected( false );
453 this.categoriesWidget
.setValue( [] );
454 this.dateWidget
.setValue( '' ).setValidityFlag( true );