diff options
Diffstat (limited to 'www/wiki/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js')
-rw-r--r-- | www/wiki/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js | 461 |
1 files changed, 461 insertions, 0 deletions
diff --git a/www/wiki/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js b/www/wiki/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js new file mode 100644 index 00000000..7d4ed537 --- /dev/null +++ b/www/wiki/resources/src/mediawiki/mediawiki.ForeignStructuredUpload.BookletLayout.js @@ -0,0 +1,461 @@ +/* global moment, Uint8Array */ +( function ( $, mw ) { + + /** + * mw.ForeignStructuredUpload.BookletLayout encapsulates the process + * of uploading a file to MediaWiki using the mw.ForeignStructuredUpload model. + * + * var uploadDialog = new mw.Upload.Dialog( { + * bookletClass: mw.ForeignStructuredUpload.BookletLayout, + * booklet: { + * target: 'local' + * } + * } ); + * var windowManager = new OO.ui.WindowManager(); + * $( 'body' ).append( windowManager.$element ); + * windowManager.addWindows( [ uploadDialog ] ); + * + * @class mw.ForeignStructuredUpload.BookletLayout + * @uses mw.ForeignStructuredUpload + * @extends mw.Upload.BookletLayout + * + * @constructor + * @param {Object} config Configuration options + * @cfg {string} [target] Used to choose the target repository. + * If nothing is passed, the {@link mw.ForeignUpload#property-target default} is used. + */ + mw.ForeignStructuredUpload.BookletLayout = function ( config ) { + config = config || {}; + // Parent constructor + mw.ForeignStructuredUpload.BookletLayout.parent.call( this, config ); + + this.target = config.target; + }; + + /* Setup */ + + OO.inheritClass( mw.ForeignStructuredUpload.BookletLayout, mw.Upload.BookletLayout ); + + /* Uploading */ + + /** + * @inheritdoc + */ + mw.ForeignStructuredUpload.BookletLayout.prototype.initialize = function () { + var booklet = this; + return mw.ForeignStructuredUpload.BookletLayout.parent.prototype.initialize.call( this ).then( + function () { + return $.when( + // Point the CategoryMultiselectWidget to the right wiki + booklet.upload.getApi().then( function ( api ) { + // If this is a ForeignApi, it will have a apiUrl, otherwise we don't need to do anything + if ( api.apiUrl ) { + // Can't reuse the same object, CategoryMultiselectWidget calls #abort on its mw.Api instance + booklet.categoriesWidget.api = new mw.ForeignApi( api.apiUrl ); + } + return $.Deferred().resolve(); + } ), + // Set up booklet fields and license messages to match configuration + booklet.upload.loadConfig().then( function ( config ) { + var + msgPromise, + isLocal = booklet.upload.target === 'local', + fields = config.fields, + msgs = config.licensemessages[ isLocal ? 'local' : 'foreign' ]; + + // Hide disabled fields + booklet.descriptionField.toggle( !!fields.description ); + booklet.categoriesField.toggle( !!fields.categories ); + booklet.dateField.toggle( !!fields.date ); + // Update form validity + booklet.onInfoFormChange(); + + // Load license messages from the remote wiki if we don't have these messages locally + // (this means that we only load messages from the foreign wiki for custom config) + if ( mw.message( 'upload-form-label-own-work-message-' + msgs ).exists() ) { + msgPromise = $.Deferred().resolve(); + } else { + msgPromise = booklet.upload.apiPromise.then( function ( api ) { + return api.loadMessages( [ + 'upload-form-label-own-work-message-' + msgs, + 'upload-form-label-not-own-work-message-' + msgs, + 'upload-form-label-not-own-work-local-' + msgs + ] ); + } ); + } + + // Update license messages + return msgPromise.then( function () { + var $labels; + booklet.$ownWorkMessage.msg( 'upload-form-label-own-work-message-' + msgs ); + booklet.$notOwnWorkMessage.msg( 'upload-form-label-not-own-work-message-' + msgs ); + booklet.$notOwnWorkLocal.msg( 'upload-form-label-not-own-work-local-' + msgs ); + + $labels = $( [ + booklet.$ownWorkMessage[ 0 ], + booklet.$notOwnWorkMessage[ 0 ], + booklet.$notOwnWorkLocal[ 0 ] + ] ); + + // Improve the behavior of links inside these labels, which may point to important + // things like licensing requirements or terms of use + $labels.find( 'a' ) + .attr( 'target', '_blank' ) + .on( 'click', function ( e ) { + // OO.ui.FieldLayout#onLabelClick is trying to prevent default on all clicks, + // which causes the links to not be openable. Don't let it do that. + e.stopPropagation(); + } ); + } ); + }, function ( errorMsg ) { + booklet.getPage( 'upload' ).$element.msg( errorMsg ); + return $.Deferred().resolve(); + } ) + ); + } + ).catch( + // Always resolve, never reject + function () { return $.Deferred().resolve(); } + ); + }; + + /** + * Returns a {@link mw.ForeignStructuredUpload mw.ForeignStructuredUpload} + * with the {@link #cfg-target target} specified in config. + * + * @protected + * @return {mw.Upload} + */ + mw.ForeignStructuredUpload.BookletLayout.prototype.createUpload = function () { + return new mw.ForeignStructuredUpload( this.target, { + parameters: { + errorformat: 'html', + errorlang: mw.config.get( 'wgUserLanguage' ), + errorsuselocal: 1, + formatversion: 2 + } + } ); + }; + + /* Form renderers */ + + /** + * @inheritdoc + */ + mw.ForeignStructuredUpload.BookletLayout.prototype.renderUploadForm = function () { + var fieldset, + layout = this; + + // These elements are filled with text in #initialize + // TODO Refactor this to be in one place + this.$ownWorkMessage = $( '<p>' ) + .addClass( 'mw-foreignStructuredUpload-bookletLayout-license' ); + this.$notOwnWorkMessage = $( '<p>' ); + this.$notOwnWorkLocal = $( '<p>' ); + + this.selectFileWidget = new OO.ui.SelectFileWidget( { + showDropTarget: true + } ); + this.messageLabel = new OO.ui.LabelWidget( { + label: $( '<div>' ).append( + this.$notOwnWorkMessage, + this.$notOwnWorkLocal + ) + } ); + this.ownWorkCheckbox = new OO.ui.CheckboxInputWidget().on( 'change', function ( on ) { + layout.messageLabel.toggle( !on ); + } ); + + fieldset = new OO.ui.FieldsetLayout(); + fieldset.addItems( [ + new OO.ui.FieldLayout( this.selectFileWidget, { + align: 'top' + } ), + new OO.ui.FieldLayout( this.ownWorkCheckbox, { + align: 'inline', + label: $( '<div>' ).append( + $( '<p>' ).text( mw.msg( 'upload-form-label-own-work' ) ), + this.$ownWorkMessage + ) + } ), + new OO.ui.FieldLayout( this.messageLabel, { + align: 'top' + } ) + ] ); + this.uploadForm = new OO.ui.FormLayout( { items: [ fieldset ] } ); + + // Validation + this.selectFileWidget.on( 'change', this.onUploadFormChange.bind( this ) ); + this.ownWorkCheckbox.on( 'change', this.onUploadFormChange.bind( this ) ); + + this.selectFileWidget.on( 'change', function () { + var file = layout.getFile(); + + // Set the date to lastModified once we have the file + if ( layout.getDateFromLastModified( file ) !== undefined ) { + layout.dateWidget.setValue( layout.getDateFromLastModified( file ) ); + } + + // Check if we have EXIF data and set to that where available + layout.getDateFromExif( file ).done( function ( date ) { + layout.dateWidget.setValue( date ); + } ); + + layout.updateFilePreview(); + } ); + + return this.uploadForm; + }; + + /** + * @inheritdoc + */ + mw.ForeignStructuredUpload.BookletLayout.prototype.onUploadFormChange = function () { + var file = this.selectFileWidget.getValue(), + ownWork = this.ownWorkCheckbox.isSelected(), + valid = !!file && ownWork; + this.emit( 'uploadValid', valid ); + }; + + /** + * @inheritdoc + */ + mw.ForeignStructuredUpload.BookletLayout.prototype.renderInfoForm = function () { + var fieldset; + + this.filePreview = new OO.ui.Widget( { + classes: [ 'mw-upload-bookletLayout-filePreview' ] + } ); + this.progressBarWidget = new OO.ui.ProgressBarWidget( { + progress: 0 + } ); + this.filePreview.$element.append( this.progressBarWidget.$element ); + + this.filenameWidget = new OO.ui.TextInputWidget( { + required: true, + validate: /.+/ + } ); + this.descriptionWidget = new OO.ui.MultilineTextInputWidget( { + required: true, + validate: /\S+/, + autosize: true + } ); + this.categoriesWidget = new mw.widgets.CategoryMultiselectWidget( { + // Can't be done here because we don't know the target wiki yet... done in #initialize. + // api: new mw.ForeignApi( ... ), + $overlay: this.$overlay + } ); + this.dateWidget = new mw.widgets.DateInputWidget( { + $overlay: this.$overlay, + required: true, + mustBeBefore: moment().add( 1, 'day' ).locale( 'en' ).format( 'YYYY-MM-DD' ) // Tomorrow + } ); + + this.filenameField = new OO.ui.FieldLayout( this.filenameWidget, { + label: mw.msg( 'upload-form-label-infoform-name' ), + align: 'top', + classes: [ 'mw-foreignStructuredUploa-bookletLayout-small-notice' ], + notices: [ mw.msg( 'upload-form-label-infoform-name-tooltip' ) ] + } ); + this.descriptionField = new OO.ui.FieldLayout( this.descriptionWidget, { + label: mw.msg( 'upload-form-label-infoform-description' ), + align: 'top', + classes: [ 'mw-foreignStructuredUploa-bookletLayout-small-notice' ], + notices: [ mw.msg( 'upload-form-label-infoform-description-tooltip' ) ] + } ); + this.categoriesField = new OO.ui.FieldLayout( this.categoriesWidget, { + label: mw.msg( 'upload-form-label-infoform-categories' ), + align: 'top' + } ); + this.dateField = new OO.ui.FieldLayout( this.dateWidget, { + label: mw.msg( 'upload-form-label-infoform-date' ), + align: 'top' + } ); + + fieldset = new OO.ui.FieldsetLayout( { + label: mw.msg( 'upload-form-label-infoform-title' ) + } ); + fieldset.addItems( [ + this.filenameField, + this.descriptionField, + this.categoriesField, + this.dateField + ] ); + this.infoForm = new OO.ui.FormLayout( { + classes: [ 'mw-upload-bookletLayout-infoForm' ], + items: [ this.filePreview, fieldset ] + } ); + + // Validation + this.filenameWidget.on( 'change', this.onInfoFormChange.bind( this ) ); + this.descriptionWidget.on( 'change', this.onInfoFormChange.bind( this ) ); + this.dateWidget.on( 'change', this.onInfoFormChange.bind( this ) ); + + this.on( 'fileUploadProgress', function ( progress ) { + this.progressBarWidget.setProgress( progress * 100 ); + }.bind( this ) ); + + return this.infoForm; + }; + + /** + * @inheritdoc + */ + mw.ForeignStructuredUpload.BookletLayout.prototype.onInfoFormChange = function () { + var layout = this, + validityPromises = []; + + validityPromises.push( this.filenameWidget.getValidity() ); + if ( this.descriptionField.isVisible() ) { + validityPromises.push( this.descriptionWidget.getValidity() ); + } + if ( this.dateField.isVisible() ) { + validityPromises.push( this.dateWidget.getValidity() ); + } + + $.when.apply( $, validityPromises ).done( function () { + layout.emit( 'infoValid', true ); + } ).fail( function () { + layout.emit( 'infoValid', false ); + } ); + }; + + /** + * @param {mw.Title} filename + * @return {jQuery.Promise} Resolves (on success) or rejects with OO.ui.Error + */ + mw.ForeignStructuredUpload.BookletLayout.prototype.validateFilename = function ( filename ) { + return ( new mw.Api() ).get( { + action: 'query', + prop: 'info', + titles: filename.getPrefixedDb(), + formatversion: 2 + } ).then( + function ( result ) { + // if the file already exists, reject right away, before + // ever firing finishStashUpload() + if ( !result.query.pages[ 0 ].missing ) { + return $.Deferred().reject( new OO.ui.Error( + $( '<p>' ).msg( 'fileexists', filename.getPrefixedDb() ), + { recoverable: false } + ) ); + } + }, + function () { + // API call failed - this could be a connection hiccup... + // Let's just ignore this validation step and turn this + // failure into a successful resolve ;) + return $.Deferred().resolve(); + } + ); + }; + + /** + * @inheritdoc + */ + mw.ForeignStructuredUpload.BookletLayout.prototype.saveFile = function () { + var title = mw.Title.newFromText( + this.getFilename(), + mw.config.get( 'wgNamespaceIds' ).file + ); + + return this.uploadPromise + .then( this.validateFilename.bind( this, title ) ) + .then( mw.ForeignStructuredUpload.BookletLayout.parent.prototype.saveFile.bind( this ) ); + }; + + /* Getters */ + + /** + * @inheritdoc + */ + mw.ForeignStructuredUpload.BookletLayout.prototype.getText = function () { + var language = mw.config.get( 'wgContentLanguage' ); + this.upload.clearDescriptions(); + this.upload.addDescription( language, this.descriptionWidget.getValue() ); + this.upload.setDate( this.dateWidget.getValue() ); + this.upload.clearCategories(); + this.upload.addCategories( this.categoriesWidget.getItemsData() ); + return this.upload.getText(); + }; + + /** + * Get original date from EXIF data + * + * @param {Object} file + * @return {jQuery.Promise} Promise resolved with the EXIF date + */ + mw.ForeignStructuredUpload.BookletLayout.prototype.getDateFromExif = function ( file ) { + var fileReader, + deferred = $.Deferred(); + + if ( file && file.type === 'image/jpeg' ) { + fileReader = new FileReader(); + fileReader.onload = function () { + var fileStr, arr, i, metadata, + jpegmeta = mw.loader.require( 'mediawiki.libs.jpegmeta' ); + + if ( typeof fileReader.result === 'string' ) { + fileStr = fileReader.result; + } else { + // Array buffer; convert to binary string for the library. + arr = new Uint8Array( fileReader.result ); + fileStr = ''; + for ( i = 0; i < arr.byteLength; i++ ) { + fileStr += String.fromCharCode( arr[ i ] ); + } + } + + try { + metadata = jpegmeta( fileStr, file.name ); + } catch ( e ) { + metadata = null; + } + + if ( metadata !== null && metadata.exif !== undefined && metadata.exif.DateTimeOriginal ) { + deferred.resolve( moment( metadata.exif.DateTimeOriginal, 'YYYY:MM:DD' ).format( 'YYYY-MM-DD' ) ); + } else { + deferred.reject(); + } + }; + + if ( 'readAsBinaryString' in fileReader ) { + fileReader.readAsBinaryString( file ); + } else if ( 'readAsArrayBuffer' in fileReader ) { + fileReader.readAsArrayBuffer( file ); + } else { + // We should never get here + deferred.reject(); + throw new Error( 'Cannot read thumbnail as binary string or array buffer.' ); + } + } + + return deferred.promise(); + }; + + /** + * Get last modified date from file + * + * @param {Object} file + * @return {Object} Last modified date from file + */ + mw.ForeignStructuredUpload.BookletLayout.prototype.getDateFromLastModified = function ( file ) { + if ( file && file.lastModified ) { + return moment( file.lastModified ).format( 'YYYY-MM-DD' ); + } + }; + + /* Setters */ + + /** + * @inheritdoc + */ + mw.ForeignStructuredUpload.BookletLayout.prototype.clear = function () { + mw.ForeignStructuredUpload.BookletLayout.parent.prototype.clear.call( this ); + + this.ownWorkCheckbox.setSelected( false ); + this.categoriesWidget.setItemsFromData( [] ); + this.dateWidget.setValue( '' ).setValidityFlag( true ); + }; + +}( jQuery, mediaWiki ) ); |