wikimedia/mediawiki-extensions-UploadWizard

View on GitHub
resources/uw.LicenseGroup.js

Summary

Maintainability
C
7 hrs
Test Coverage
( function ( uw ) {

    /**
     * @extends OO.ui.LicenseGroup
     *
     * @class
     * @inheritdoc
     * @param {Object} config License configuration
     * @param {Array} config.licenses Array of license names
     * @param {string} [config.head] Header for the group of licenses (if present, the group of
     *   licenses will be collapsed and this will be the clickable title to expand the group)
     * @param {string} [config.subhead] Subtitle for the group of licenses
     * @param {string} [config.special] 'custom' if a text input field should be added
     * @param {string} [config.template] Filter templates. If 'filterTemplate' was 'filter',
     *   then  [ 'fooLicense', 'barLicense' ] -> {{filter|fooLicense|barLicense}}
     * @param {Array} [config.prependTemplates] Array of templates to prepend. If prependTemplates
     *   were [ 'pre', 'pended' ], then [ 'fooLicense' ] -> "{{pre}}{{pended}}{{fooLicense}}"
     * @param {string} type 'radio' or 'checkbox'
     * @param {mw.Api} api API object, used for wikitext previews
     * @param {number} count Number of the things we are licensing (it matters to some texts)
     */
    uw.LicenseGroup = function UWLicenseGroup( config, type, api, count ) {
        var self = this;

        uw.LicenseGroup.super.call( this, {} );

        if ( typeof config.licenses !== 'object' ) {
            throw new Error( 'improper license config' );
        }

        if ( [ 'radio', 'checkbox' ].indexOf( type ) < 0 ) {
            throw new Error( 'Invalid type: ' + type );
        }

        this.config = config;
        this.type = type;
        this.api = api;
        this.count = count;
        this.customInputs = {};
        this.previewDialog = new uw.LicensePreviewDialog();
        this.windowManager = new OO.ui.WindowManager();
        this.windowManager.addWindows( [ this.previewDialog ] );
        $( document.body ).append( this.windowManager.$element );

        if ( this.type === 'radio' ) {
            this.group = this.createRadioGroup( [ 'mwe-upwiz-deed-license-group-body' ] );
            this.group.connect( this, { choose: [ 'emit', 'change', this ] } );
        } else if ( this.type === 'checkbox' ) {
            this.group = this.createCheckboxGroup( [ 'mwe-upwiz-deed-license-group-body' ] );
            this.group.connect( this, { select: [ 'emit', 'change', this ] } );
        }

        // when selecting an item that has a custom input, we'll immediately focus it
        this.on( 'change', ( group, item ) => {
            if ( item && item.isSelected && item.isSelected() ) {
                // wrapped inside setTimeout to ensure it goes at the end of the call stack,
                // just in case something steals focus in the meantime...
                setTimeout( () => {
                    var name = item.getData();
                    if ( self.customInputs[ name ] ) {
                        self.customInputs[ name ].focus();
                    }
                } );
            }
        } );

        this.fieldset = this.createFieldset( this.group );
        this.$element = this.fieldset.$element;
    };
    OO.inheritClass( uw.LicenseGroup, OO.ui.Widget );

    uw.LicenseGroup.prototype.unload = function () {
        this.windowManager.$element.remove();
    };

    /**
     * @param {OO.ui.RadioSelectWidget|OO.ui.CheckboxMultiselectInputWidget} group
     * @return {OO.ui.FieldsetLayout}
     */
    uw.LicenseGroup.prototype.createFieldset = function ( group ) {
        var fieldset = new OO.ui.FieldsetLayout( {
                items: [ group ],
                classes: [ 'mwe-upwiz-deed-license-group' ]
            } ),
            labelParams, $subhead;

        if ( this.config.subhead ) {
            // 'url' can be either a single (string) url, or an array of (string) urls;
            // hence this convoluted variable-length parameters assembly...
            labelParams = [ this.config.subhead, this.count ].concat( this.config.url );
            $subhead = $( '<div>' )
                .addClass( 'mwe-upwiz-deed-license-group-subhead mwe-upwiz-deed-title' )
                .append( mw.message.apply( mw.message, labelParams ).parseDom() );

            fieldset.addItems(
                [
                    new OO.ui.FieldLayout(
                        new OO.ui.Widget( { content: [] } ),
                        { label: $subhead, align: 'top' }
                    )
                ],
                0 // = index; add to top
            );
        }

        return fieldset;
    };

    /**
     * @param {Array} classes to add
     * @return {OO.ui.RadioSelectWidget}
     */
    uw.LicenseGroup.prototype.createRadioGroup = function ( classes ) {
        var self = this,
            options = [];

        this.config.licenses.forEach( ( licenseName ) => {
            var option;

            if ( mw.UploadWizard.config.licenses[ licenseName ] === undefined ) {
                // unknown license
                return;
            }

            option = new OO.ui.RadioOptionWidget( {
                label: self.createLabel( licenseName ),
                data: licenseName
            } );

            // when custom text area receives focus, we should make sure this element is selected
            if ( self.customInputs[ licenseName ] ) {
                self.customInputs[ licenseName ].on( 'focus', () => {
                    option.setSelected( true );
                } );
            }

            options.push( option );
        } );

        return new OO.ui.RadioSelectWidget( { items: options, classes: classes } );
    };

    /**
     * @param {Array} classes to add
     * @return {OO.ui.CheckboxMultiselectInputWidget}
     */
    uw.LicenseGroup.prototype.createCheckboxGroup = function ( classes ) {
        var self = this,
            options = [];

        this.config.licenses.forEach( ( licenseName ) => {
            var option;

            if ( mw.UploadWizard.config.licenses[ licenseName ] === undefined ) {
                // unknown license
                return;
            }

            option = new OO.ui.CheckboxMultioptionWidget( {
                label: self.createLabel( licenseName ),
                data: licenseName
            } );

            // when custom input fields changes, we should make sure this element is selected when
            // there is content, or deselected when empty
            if ( self.customInputs[ licenseName ] ) {
                self.customInputs[ licenseName ].on( 'focus', () => {
                    option.setSelected( true );
                } );
            }

            options.push( option );
        } );

        return new OO.ui.CheckboxMultiselectWidget( { items: options, classes: classes } );
    };

    /**
     * @return {string}
     */
    uw.LicenseGroup.prototype.getWikiText = function () {
        var wikiTexts,
            self = this,
            values = this.getValue();

        wikiTexts = Object.keys( values ).map( ( name ) => {
            var wikiText = self.getLicenceWikiText( name ),
                value = values[ name ];
            if ( typeof value === 'string' ) {
                // `value` is custom input
                wikiText += '\n' + value.trim();
            }
            return wikiText;
        } );

        return wikiTexts.join( '' ).trim();
    };

    /**
     * Returns a string unique to the group (if defined)
     *
     * @return {string}
     */
    uw.LicenseGroup.prototype.getGroup = function () {
        return this.config.head || '';
    };

    /**
     * @return {string}
     */
    uw.LicenseGroup.prototype.getData = function () {
        return this.getGroup();
    };

    /**
     * @return {Object} Map of { licenseName: true }, or { licenseName: "custom input" }
     */
    uw.LicenseGroup.prototype.getValue = function () {
        var self = this,
            result = {},
            selected,
            name;

        if ( this.type === 'radio' ) {
            selected = this.group.findSelectedItem();
            if ( selected ) {
                name = selected.getData();
                result[ name ] = !this.customInputs[ name ] || this.customInputs[ name ].getValue();
            }
        } else if ( this.type === 'checkbox' ) {
            selected = this.group.findSelectedItems();
            selected.forEach( ( item ) => {
                name = item.getData();
                result[ name ] = !self.customInputs[ name ] || self.customInputs[ name ].getValue();
            } );
        }

        return result;
    };

    /**
     * @param {Object} values Map of { licenseName: true }, or { licenseName: "custom input" }
     */
    uw.LicenseGroup.prototype.setValue = function ( values ) {
        var self = this,
            selectArray = [];

        Object.keys( values ).forEach( ( name ) => {
            var value = values[ name ];
            if ( typeof value === 'string' && self.customInputs[ name ] ) {
                self.customInputs[ name ].setValue( value );
                // add to list of items to select
                selectArray.push( name );
            }

            // add to list of items to select
            // (only true/string values should be included in `values`, but might
            // as well play it safe...)
            if ( value === true ) {
                selectArray.push( name );
            }
        } );

        if ( this.type === 'radio' ) {
            this.group.selectItemByData( selectArray[ 0 ] );
        } else if ( this.type === 'checkbox' ) {
            this.group.selectItemsByData( selectArray );
        }
    };

    /**
     * @private
     * @param {string} name
     * @return {Object}
     */
    uw.LicenseGroup.prototype.getLicenseInfo = function ( name ) {
        return {
            name: name,
            props: mw.UploadWizard.config.licenses[ name ]
        };
    };

    /**
     * @private
     * @param {string} name
     * @return {string[]}
     */
    uw.LicenseGroup.prototype.getTemplates = function ( name ) {
        var licenseInfo = this.getLicenseInfo( name );
        return licenseInfo.props.templates === undefined ?
            [ licenseInfo.name ] :
            licenseInfo.props.templates.slice( 0 );
    };

    /**
     * License templates are these abstract ideas like cc-by-sa. In general they map directly to a license template.
     * However, configuration for a particular option can add other templates or transform the templates,
     * such as wrapping templates in an outer "self" template for own-work
     *
     * @private
     * @param {string} name license template name
     * @return {string} of wikitext
     */
    uw.LicenseGroup.prototype.getLicenceWikiText = function ( name ) {
        var templates = this.getTemplates( name ),
            wikiTexts;

        if ( this.config.prependTemplates !== undefined ) {
            this.config.prependTemplates.forEach( ( template ) => {
                templates.unshift( template );
            } );
        }

        if ( this.config.template !== undefined ) {
            templates.unshift( this.config.template );
            templates = [ templates.join( '|' ) ];
        }

        wikiTexts = templates.map( ( t ) => '{{' + t + '}}' );
        return wikiTexts.join( '' );
    };

    /**
     * Get a label for the form element
     *
     * @private
     * @param {string} name license template name
     * @return {jQuery}
     */
    uw.LicenseGroup.prototype.createLabel = function ( name ) {
        var licenseInfo = this.getLicenseInfo( name ),
            messageKey = licenseInfo.props.msg === undefined ?
                '[missing msg for ' + licenseInfo.name + ']' :
                licenseInfo.props.msg,
            // The URL is optional, but if the message includes it as $2, we surface the fact
            // that it's missing.
            licenseURL = licenseInfo.props.url === undefined ? '#missing license URL' : licenseInfo.props.url,
            $licenseLink,
            $icons = $( '<span>' ),
            $label;

        if (
            licenseInfo.props.languageCodePrefix !== undefined &&
            licenseInfo.props.availableLanguages !== undefined
        ) {
            var i,
                targetLanguageCode = 'en', // final fallback
                fallbackChain = mw.language.getFallbackLanguageChain();
            for ( i = 0; i < fallbackChain.length; i++ ) {
                if ( licenseInfo.props.availableLanguages.indexOf( fallbackChain[ i ] ) !== -1 ) {
                    targetLanguageCode = fallbackChain[ i ];
                    break;
                }
            }
            licenseURL += licenseInfo.props.languageCodePrefix + targetLanguageCode;
        }
        $licenseLink = $( '<a>' ).attr( { target: '_blank', href: licenseURL } );
        if ( licenseInfo.props.icons !== undefined ) {
            licenseInfo.props.icons.forEach( ( icon ) => {
                $icons.append( $( '<span>' ).addClass( 'mwe-upwiz-license-icon mwe-upwiz-' + icon + '-icon' ) );
            } );
        }

        $label = $( '<label>' )
            .msg( messageKey, this.count || 0, $licenseLink, $icons )
            .addClass( 'mwe-upwiz-copyright-info' );

        if ( this.config.special === 'custom' ) {
            $label.append( this.createCustom( name, licenseInfo.props.defaultText ) );
            $label.append(
                $( '<span>' )
                    .msg( 'mwe-upwiz-license-custom-explain', this.count || 0, $licenseLink )
                    .addClass( 'mwe-upwiz-label-extra' )
            );
        }

        if ( licenseInfo.props.msgExplain !== undefined ) {
            $label.append(
                $( '<span>' )
                    .msg( licenseInfo.props.msgExplain, this.count || 0, $licenseLink )
                    .addClass( 'mwe-upwiz-label-extra mwe-upwiz-label-explainer' )
            );
        }

        return $label.contents();
    };

    /**
     * @private
     * @param {string} name license name
     * @param {string} [defaultText] Default custom license text
     * @return {jQuery} Wrapped textarea
     */
    uw.LicenseGroup.prototype.createCustom = function ( name, defaultText ) {
        var self = this,
            button;

        this.customInputs[ name ] = new OO.ui.TextInputWidget( {
            value: defaultText
        } );

        // Update displayed errors as the user is typing
        this.customInputs[ name ].on( 'change', OO.ui.debounce( this.emit.bind( this, 'change', this ), 500 ) );

        button = new OO.ui.ButtonWidget( {
            label: mw.message( 'mwe-upwiz-license-custom-preview' ).text(),
            flags: [ 'progressive' ]
        } ).on( 'click', () => {
            self.showPreview( self.customInputs[ name ].getValue() );
        } );

        return $( '<div>' ).addClass( 'mwe-upwiz-license-custom' ).append(
            this.customInputs[ name ].$element,
            button.$element
        );
    };

    /**
     * Preview wikitext in a popup window
     *
     * @private
     * @param {string} wikiText
     */
    uw.LicenseGroup.prototype.showPreview = function ( wikiText ) {
        var input;

        this.previewDialog.setLoading( true );
        this.windowManager.openWindow( this.previewDialog );

        input = this;

        function show( html ) {
            input.previewDialog.setPreview( html );
            input.windowManager.openWindow( input.previewDialog );
        }

        function error( code, result ) {
            var message = result.errors[ 0 ].html;

            show( $( '<div>' ).append(
                $( '<h3>' ).append( code ),
                $( '<p>' ).append( message )
            ) );
        }

        this.api.parse( wikiText, { pst: true } ).done( show ).fail( error );
    };

}( mw.uploadWizard ) );