wikimedia/mediawiki-extensions-WikiLove

View on GitHub
resources/ext.wikiLove.startup/wikiLove.js

Summary

Maintainability
F
1 wk
Test Coverage
/* eslint-disable no-jquery/no-global-selector */
const overlayContainer = document.createElement( 'div' );
const WikiLoveDialog = require( './WikiLoveDialog.vue' );
const Vue = require( 'vue' );
let options = {}, // options modifiable by the user
    currentTypeId = null, // id of the currently selected type (e.g. 'barnstar' or 'makeyourown')
    currentSubtypeId = null, // id of the currently selected subtype (e.g. 'original' or 'special')
    currentTypeOrSubtype = null, // content of the current (sub)type (i.e. an object with title, descr, text, etc.)
    rememberData = null, // input data to remember when switching types or subtypes
    emailable = false, // whether or not the target user is emailable
    redirect = true, // whether or not to redirect the user to the WikiLove message after it has been posted
    targets = []; // the recipients of the WikiLove
const maxRecipients = 10; // maximum number of simultaneous recipients
let gallery = {};
const api = new mw.Api();

module.exports = {
    /**
     * Opens the dialog and builds it if necessary.
     *
     * @param {string[]} recipients Usernames of recipients (without namespace prefix)
     * @param {string[]} [extraTags] Extra tags to apply
     */
    openDialog: function ( recipients, extraTags ) {
        let type, typeId, $button;
        // If a list of recipients are specified, this will override the normal
        // behavior of WikiLove, which is to post on the Talk page of the
        // current page. It will also disable redirecting the user after submitting.
        if ( recipients ) {
            if ( recipients.length > maxRecipients ) {
                // TODO: Don't use window.alert
                // eslint-disable-next-line no-alert
                alert( mw.msg( 'wikilove-err-max-exceeded', maxRecipients ) );
                return;
            }
            targets = recipients;
            redirect = false;
            // TODO: See if recipients are emailable
        } else {
            targets.push( mw.config.get( 'wgTitle' ) );
            // Test to see if the 'E-mail this user' link exists
            emailable = !!$( '#t-emailuser' ).length;
        }

        options.extraTags = extraTags || [];

        // Build a type list like this:
        const $typeList = $( '<ul>' ).attr( 'id', 'mw-wikilove-types' );
        for ( typeId in options.types ) {
            type = options.types[ typeId ];
            if ( !$.isPlainObject( type ) ) {
                continue;
            }
            $button = $( '<a>' ).attr( 'href', '#' );

            if ( typeof type.icon === 'string' ) {
                $button.append( $( '<img>' ).attr( 'src', type.icon ) );
            } else {
                $button.addClass( 'mw-wikilove-no-icon' );
            }

            $button.append( $( '<span>' ).text( type.name ) );

            $button.data( 'typeId', typeId );
            $typeList.append( $( '<li>' ).append( $button ) );
        }

        const commonsLink = mw.html.element( 'a', {
            href: mw.msg( 'wikilove-commons-url' ),
            target: '_blank'
        }, mw.msg( 'wikilove-commons-link' ) );
        const termsLink = mw.html.element( 'a', {
            href: mw.msg( 'wikilove-terms-url' ),
            target: '_blank'
        }, mw.msg( 'wikilove-terms-link' ) );

        overlayContainer.classList.add( 'wikilove-overlay-container' );
        overlayContainer.style.display = '';

        if ( overlayContainer.parentNode ) {
            return;
        }
        // render.
        document.body.appendChild( overlayContainer );
        Vue.createMwApp( WikiLoveDialog, {
            commonsLink,
            termsLink,
            onClose: () => {
                this.reset();
            }
        } ).mount( overlayContainer );

        // @todo: Move logic to WikiLoveDialog.vue
        $( '#mw-wikilove-add-details' ).hide();
        $( '#mw-wikilove-preview' ).hide();
        $( '#mw-wikilove-anon-warning' ).hide();
        $( '#mw-wikilove-types' ).replaceWith( $typeList );
        $( '#mw-wikilove-gallery-error-again' ).on( 'click', $.wikiLove.showGallery );
        $( '#mw-wikilove-types a' ).on( 'click', $.wikiLove.clickType );
        $( '#mw-wikilove-subtype' ).on( 'change', $.wikiLove.changeSubtype );
        $( '#mw-wikilove-preview-form' ).on( 'submit', $.wikiLove.validatePreviewForm );
        $( '#mw-wikilove-send-form' ).on( 'click', $.wikiLove.submitSend );

        if ( mw.util.isIPAddress( mw.config.get( 'wikilove-recipient' ) ) ||
            mw.util.isTemporaryUser( mw.config.get( 'wikilove-recipient' ) )
        ) {
            $( '#mw-wikilove-anon-warning' ).show();
        }

        // When the image changes, we want to reset the preview and error message.
        $( '#mw-wikilove-image' ).on( 'change', () => {
            $( '#mw-wikilove-dialog' ).find( '.mw-wikilove-error' ).remove();
            $( '#mw-wikilove-preview' ).hide();
        } );
    },

    /**
     * Handler for the left menu. Selects a new type and initialises next section
     * depending on whether or not to show subtypes.
     *
     * @param {jQuery.Event} e Click event
     */
    clickType: function ( e ) {
        let subtypeId, subtype;
        const newTypeId = $( this ).data( 'typeId' );

        e.preventDefault();
        $.wikiLove.rememberInputData(); // remember previously entered data
        $( '#mw-wikilove-get-started' ).hide(); // always hide the get started section

        if ( currentTypeId !== newTypeId ) { // only do stuff when a different type is selected
            currentTypeId = newTypeId;
            currentSubtypeId = null; // reset the subtype id

            $( '#mw-wikilove-types' ).find( 'a' ).removeClass( 'selected' );
            $( this ).addClass( 'selected' ); // highlight the new type in the menu

            if ( typeof options.types[ currentTypeId ].subtypes === 'object' ) {
                // we're dealing with subtypes here
                currentTypeOrSubtype = null; // reset the (sub)type object until a subtype is selected
                $( '#mw-wikilove-subtype' ).empty(); // clear the subtype menu

                for ( subtypeId in options.types[ currentTypeId ].subtypes ) {
                    // add all the subtypes to the menu while setting their subtype ids in jQuery data
                    subtype = options.types[ currentTypeId ].subtypes[ subtypeId ];
                    if ( typeof subtype.option !== 'undefined' ) {
                        $( '#mw-wikilove-subtype' ).append(
                            $( '<option>' ).text( subtype.option ).data( 'subtypeId', subtypeId )
                        );
                    }
                }
                $( '#mw-wikilove-subtype' ).show();

                // change and show the subtype label depending on the type
                $( '#mw-wikilove-subtype-label' ).text(
                    options.types[ currentTypeId ].select || mw.msg( 'wikilove-select-type' )
                );
                $( '#mw-wikilove-subtype-label' ).show();
                $.wikiLove.changeSubtype(); // update controls depending on the currently selected (i.e. first) subtype
            } else {
                // there are no subtypes, just use this type for the current (sub)type
                currentTypeOrSubtype = options.types[ currentTypeId ];
                $( '#mw-wikilove-subtype' ).hide();
                $( '#mw-wikilove-subtype-label' ).hide();
                $( '#mw-wikilove-image-preview' ).hide();
                $.wikiLove.updateAllDetails(); // update controls depending on this type
            }

            $( '#mw-wikilove-add-details' ).show();
            $( '#mw-wikilove-preview' ).hide();
        }
    },

    /**
     * Handler for changing the subtype.
     */
    changeSubtype: function () {
        // eslint-disable-next-line no-jquery/no-sizzle
        const newSubtypeId = $( '#mw-wikilove-subtype option:selected' ).first().data( 'subtypeId' );

        $.wikiLove.rememberInputData(); // remember previously entered data

        // find out which subtype is selected
        if ( currentSubtypeId !== newSubtypeId ) { // only change stuff when a different subtype is selected
            currentSubtypeId = newSubtypeId;
            currentTypeOrSubtype = options.types[ currentTypeId ]
                .subtypes[ currentSubtypeId ];

            if ( currentTypeOrSubtype.gallery === undefined && currentTypeOrSubtype.image ) { // not a gallery
                $.wikiLove.showImagePreview();
            } else {
                $( '#mw-wikilove-image-preview' ).hide();
            }

            $.wikiLove.updateAllDetails();
            $( '#mw-wikilove-preview' ).hide();
        }
    },

    /**
     * Remember data the user entered if it is different from the default.
     */
    rememberInputData: function () {
        if ( rememberData === null ) {
            rememberData = {
                header: '',
                title: '',
                message: '',
                image: ''
            };
        }
        if ( currentTypeOrSubtype !== null ) {
            if ( currentTypeOrSubtype.fields.indexOf( 'header' ) !== -1 &&
                ( !currentTypeOrSubtype.header || $( '#mw-wikilove-header' ).val() !== currentTypeOrSubtype.header )
            ) {
                rememberData.header = $( '#mw-wikilove-header' ).val();
            }
            if ( currentTypeOrSubtype.fields.indexOf( 'title' ) !== -1 &&
                ( !currentTypeOrSubtype.title || $( '#mw-wikilove-title' ).val() !== currentTypeOrSubtype.title )
            ) {
                rememberData.title = $( '#mw-wikilove-title' ).val();
            }
            if ( currentTypeOrSubtype.fields.indexOf( 'message' ) !== -1 &&
                ( !currentTypeOrSubtype.message || $( '#mw-wikilove-message' ).val() !== currentTypeOrSubtype.message )
            ) {
                rememberData.message = $( '#mw-wikilove-message' ).val();
            }
            if ( currentTypeOrSubtype.gallery === undefined && currentTypeOrSubtype.fields.indexOf( 'image' ) !== -1 &&
                ( !currentTypeOrSubtype.image || $( '#mw-wikilove-image' ).val() !== currentTypeOrSubtype.image )
            ) {
                rememberData.image = $( '#mw-wikilove-image' ).val();
            }
        }
    },

    /**
     * Show a preview of the image for a subtype.
     */
    showImagePreview: function () {
        let $img;
        const title = $.wikiLove.normalizeFilename( currentTypeOrSubtype.image );
        const loadingType = currentTypeOrSubtype;
        $( '#mw-wikilove-image-preview' ).show();
        $( '#mw-wikilove-image-preview-content' ).empty();
        // TODO: Use CSS transitions
        // eslint-disable-next-line no-jquery/no-fade
        $( '#mw-wikilove-image-preview-spinner' ).fadeIn( 200 );
        api.post( {
            formatversion: 2,
            action: 'query',
            prop: 'imageinfo',
            iiprop: 'mime|url',
            titles: title,
            iiurlwidth: 75,
            iiurlheight: 68
        } )
            .done( ( data ) => {
                if ( !data || !data.query || !data.query.pages ) {
                    // TODO: Use CSS transitions
                    // eslint-disable-next-line no-jquery/no-fade
                    $( '#mw-wikilove-image-preview-spinner' ).fadeOut( 200 );
                    return;
                }
                if ( loadingType !== currentTypeOrSubtype ) {
                    return;
                }
                data.query.pages.forEach( ( page ) => {
                    if ( page.imageinfo && page.imageinfo.length ) {
                        // build an image tag with the correct url
                        $img = $( '<img>' )
                            .attr( 'src', page.imageinfo[ 0 ].thumburl )
                            .hide()
                            .on( 'load', function () {
                                $( '#mw-wikilove-image-preview-spinner' ).hide();
                                $( this ).css( 'display', 'inline-block' );
                            } );
                        $( '#mw-wikilove-image-preview-content' ).append( $img );
                    }
                } );
            } )
            .fail( () => {
                // TODO: Use CSS transitions
                // eslint-disable-next-line no-jquery/no-fade
                $( '#mw-wikilove-image-preview-spinner' ).fadeOut( 200 );
            } );
    },

    /**
     * Called when type or subtype changes, updates controls.
     */
    updateAllDetails: function () {
        // use remembered data for fields that can be set by the user
        const currentRememberData = {
            header: ( currentTypeOrSubtype.fields.indexOf( 'header' ) !== -1 ? rememberData.header : '' ),
            title: ( currentTypeOrSubtype.fields.indexOf( 'title' ) !== -1 ? rememberData.title : '' ),
            message: ( currentTypeOrSubtype.fields.indexOf( 'message' ) !== -1 ? rememberData.message : '' ),
            image: ( currentTypeOrSubtype.fields.indexOf( 'image' ) !== -1 ? rememberData.image : '' )
        };

        $( '#mw-wikilove-dialog' ).find( '.mw-wikilove-error' ).remove();

        // only show the description if it exists for this type or subtype
        if ( typeof currentTypeOrSubtype.descr === 'string' ) {
            // Replace {{SITENAME}} in messages. Yes, we could have mediawiki.jqueryMsg
            // handle this, but this is a much more lightweight solution.
            currentTypeOrSubtype.descr = currentTypeOrSubtype.descr.replace( /\{\{SITENAME\}\}/g, mw.config.get( 'wgSiteName' ) );
            $( '#mw-wikilove-subtype-description' ).text( currentTypeOrSubtype.descr );
            $( '#mw-wikilove-subtype-description' ).show();
        } else {
            $( '#mw-wikilove-subtype-description' ).hide();
        }

        // show or hide header label and textbox depending on fields configuration
        $( '#mw-wikilove-header, #mw-wikilove-header-label' )
            .toggle( currentTypeOrSubtype.fields.indexOf( 'header' ) !== -1 );

        // set the new text for the header textbox
        $( '#mw-wikilove-header' ).val( currentRememberData.header || currentTypeOrSubtype.header || '' );

        // show or hide title label and textbox depending on fields configuration
        $( '#mw-wikilove-title, #mw-wikilove-title-label' )
            .toggle( currentTypeOrSubtype.fields.indexOf( 'title' ) !== -1 );

        // set the new text for the title textbox
        $( '#mw-wikilove-title' ).val( currentRememberData.title || currentTypeOrSubtype.title || '' );

        // show or hide image label and textbox depending on fields configuration
        $( '#mw-wikilove-image, #mw-wikilove-image-label, #mw-wikilove-image-note, #mw-wikilove-commons-text' )
            .toggle( currentTypeOrSubtype.fields.indexOf( 'image' ) !== -1 );

        // set the new text for the image textbox
        $( '#mw-wikilove-image' ).val( currentRememberData.image || currentTypeOrSubtype.image || '' );

        if ( typeof currentTypeOrSubtype.gallery === 'object' &&
            Array.isArray( currentTypeOrSubtype.gallery.imageList )
        ) {
            $( '#mw-wikilove-gallery, #mw-wikilove-gallery-label' ).show();
            $.wikiLove.showGallery(); // build gallery from array of images
        } else {
            $( '#mw-wikilove-gallery, #mw-wikilove-gallery-label' ).hide();
        }

        // show or hide message label and textbox depending on fields configuration
        $( '#mw-wikilove-message, #mw-wikilove-message-label, #mw-wikilove-message-note' )
            .toggle( currentTypeOrSubtype.fields.indexOf( 'message' ) !== -1 );

        // set the new text for the message textbox
        $( '#mw-wikilove-message' ).val( currentRememberData.message || currentTypeOrSubtype.message || '' );

        if ( currentTypeOrSubtype.fields.indexOf( 'notify' ) !== -1 && emailable ) {
            $( '#mw-wikilove-notify' ).show();
        } else {
            $( '#mw-wikilove-notify' ).hide();
            $( '#mw-wikilove-notify-checkbox' ).prop( 'checked', false );
        }
    },

    /**
     * Handler for clicking the preview button.
     *
     * @param {jQuery.Event} e Click event
     * @return {boolean} Event propagates
     */
    validatePreviewForm: function ( e ) {
        let imageTitle;

        e.preventDefault();
        $( '#mw-wikilove-success' ).hide();
        $( '#mw-wikilove-dialog' ).find( '.mw-wikilove-error' ).remove();
        $( '#mw-wikilove-preview' ).hide();

        // Check for a header if it is required
        if ( currentTypeOrSubtype.fields.indexOf( 'header' ) !== -1 && $( '#mw-wikilove-header' ).val().length === 0 ) {
            $.wikiLove.showAddDetailsError( 'wikilove-err-header' );
            return false;
        }

        // Check for a title if it is required, and otherwise use the header text
        if ( currentTypeOrSubtype.fields.indexOf( 'title' ) !== -1 && $( '#mw-wikilove-title' ).val().length === 0 ) {
            $( '#mw-wikilove-title' ).val( $( '#mw-wikilove-header' ).val() );
        }

        if ( currentTypeOrSubtype.fields.indexOf( 'message' ) !== -1 ) {
            // Check for a message if it is required
            if ( $( '#mw-wikilove-message' ).val().length <= 0 ) {
                $.wikiLove.showAddDetailsError( 'wikilove-err-msg' );
                return false;
            }
            // If there's a signature already in the message, throw an error
            if ( $( '#mw-wikilove-message' ).val().indexOf( '~~~' ) >= 0 ) {
                $.wikiLove.showAddDetailsError( 'wikilove-err-sig' );
                return false;
            }
        }

        // Split image validation depending on whether or not it is a gallery
        if ( typeof currentTypeOrSubtype.gallery === 'undefined' ) { // not a gallery
            if ( currentTypeOrSubtype.fields.indexOf( 'image' ) !== -1 ) { // asks for an image
                if ( $( '#mw-wikilove-image' ).val().length === 0 ) { // no image entered
                    // Give them the default image and continue with preview.
                    $( '#mw-wikilove-image' ).val( options.defaultImage );
                    $.wikiLove.submitPreview();
                } else { // image was entered by user
                    // Make sure the image exists
                    imageTitle = $.wikiLove.normalizeFilename( $( '#mw-wikilove-image' ).val() );
                    // TODO: Use CSS transitions
                    // eslint-disable-next-line no-jquery/no-fade
                    $( '#mw-wikilove-preview-spinner' ).fadeIn( 200 );

                    api.get( {
                        formatversion: 2,
                        action: 'query',
                        titles: imageTitle,
                        prop: 'imageinfo'
                    } )
                        .done( ( data ) => {
                            const page = data.query.pages[ 0 ];
                            // See if image exists locally or through InstantCommons
                            if ( !page.missing || page.imageinfo ) {
                                // Image exists
                                $.wikiLove.submitPreview();
                            } else {
                                // Image does not exist
                                $.wikiLove.showAddDetailsError( 'wikilove-err-image-bad' );
                                // TODO: Use CSS transitions
                                // eslint-disable-next-line no-jquery/no-fade
                                $( '#mw-wikilove-preview-spinner' ).fadeOut( 200 );
                            }
                        } )
                        .fail( () => {
                            $.wikiLove.showAddDetailsError( 'wikilove-err-image-api' );
                            // TODO: Use CSS transitions
                            // eslint-disable-next-line no-jquery/no-fade
                            $( '#mw-wikilove-preview-spinner' ).fadeOut( 200 );
                        } );
                }
            } else { // doesn't ask for an image
                $.wikiLove.submitPreview();
            }
        } else { // a gallery
            if ( $( '#mw-wikilove-image' ).val().length === 0 ) { // no image selected
                // Display an error telling them to select an image.
                $.wikiLove.showAddDetailsError( 'wikilove-err-image' );
                return false;
            } else { // image was selected
                $.wikiLove.submitPreview();
            }
        }
        return true;
    },

    /**
     * After the form is validated, perform preview.
     */
    submitPreview: function () {
        const text = $.wikiLove.prepareMsg( currentTypeOrSubtype.text || options.types[ currentTypeId ].text || options.defaultText );
        $.wikiLove.doPreview( text, $( '#mw-wikilove-header' ).val() );
    },

    showAddDetailsError: function ( errmsg ) {
        // eslint-disable-next-line mediawiki/msg-doc
        $( '#mw-wikilove-add-details' ).append( $( '<div>' ).addClass( 'mw-wikilove-error' ).text( mw.msg( errmsg ) ) );
    },

    showPreviewError: function ( errmsg ) {
        // eslint-disable-next-line mediawiki/msg-doc
        $( '#mw-wikilove-preview' ).append( $( '<div>' ).addClass( 'mw-wikilove-error' ).text( mw.msg( errmsg ) ) );
    },

    showSuccessMsg: function ( msg ) {
        $( '#mw-wikilove-success' ).text( msg ).show();
    },

    /**
     * Prepares a message or e-mail body by replacing placeholders.
     * $1: message entered by the user
     * $2: title of the item
     * $3: title of the image
     * $4: image size
     * $5: background color
     * $6: border color
     * $7: username of the recipient
     *
     * @param {string} msg Message with placeholders
     * @return {string} Prepared message
     */
    prepareMsg: function ( msg ) {
        msg = msg.replace( '$1', $( '#mw-wikilove-message' ).val() ); // replace the raw message
        msg = msg.replace( '$2', $( '#mw-wikilove-title' ).val() ); // replace the title
        msg = msg.replace( '$3', $.wikiLove.normalizeFilename( $( '#mw-wikilove-image' ).val() ) ); // replace the image
        msg = msg.replace( '$4', currentTypeOrSubtype.imageSize || options.defaultImageSize ); // replace the image size
        msg = msg.replace( '$5', currentTypeOrSubtype.backgroundColor || options.defaultBackgroundColor ); // replace the background color
        msg = msg.replace( '$6', currentTypeOrSubtype.borderColor || options.defaultBorderColor ); // replace the border color
        msg = msg.replace( '$7', '<nowiki>' + mw.config.get( 'wikilove-recipient', '' ) + '</nowiki>' ); // replace the username we're sending to

        return msg;
    },

    /**
     * Normalize a filename.
     * This function will extract a filename from a URL or add a "File:" prefix if there isn't
     * already a media namespace prefix.
     *
     * @param {string} filename Filename or URL from user input
     * @return {string} Normalized filename with prefix
     */
    normalizeFilename: function ( filename ) {
        const title = mw.Title.newFromImg( { src: filename } ) || mw.Title.newFromText( filename, mw.config.get( 'wgNamespaceIds' ).file );
        if ( !title ) {
            return filename;
        }
        return title.getPrefixedText();
    },

    /**
     * Fires AJAX request for previewing wikitext.
     *
     * @param {string} wikitext Body of the message
     * @param {string} sectiontitle Title of the message
     */
    doPreview: function ( wikitext, sectiontitle ) {
        // TODO: Use CSS transitions
        // eslint-disable-next-line no-jquery/no-fade
        $( '#mw-wikilove-preview-spinner' ).fadeIn( 200 );
        api.parse( wikitext, {
            prop: 'text',
            title: mw.config.get( 'wgPageName' ),
            section: 'new',
            sectiontitle: sectiontitle,
            disableeditsection: true,
            sectionpreview: true,
            pst: true
        } )
            .done( ( html ) => {
                $.wikiLove.showPreview( html );
                // TODO: Use CSS transitions
                // eslint-disable-next-line no-jquery/no-fade
                $( '#mw-wikilove-preview-spinner' ).fadeOut( 200 );
            } )
            .fail( () => {
                $.wikiLove.showAddDetailsError( 'wikilove-err-preview-api' );
                // TODO: Use CSS transitions
                // eslint-disable-next-line no-jquery/no-fade
                $( '#mw-wikilove-preview-spinner' ).fadeOut( 200 );
            } );
    },

    /**
     * Callback for the preview function. Sets the preview area with the HTML and fades it in.
     *
     * @param {string} html HTML to preview
     */
    showPreview: function ( html ) {
        $( '#mw-wikilove-preview-area' ).html( html );
        // TODO: Use CSS transitions
        // eslint-disable-next-line no-jquery/no-fade
        $( '#mw-wikilove-preview' ).fadeIn( 200 );
    },

    /**
     * Handler for the send (final submit) button. Builds the data for the AJAX request.
     * The type sent for statistics is 'typeId-subtypeId' when using subtypes,
     * or simply 'typeId' otherwise.
     *
     * @param {jQuery.Event} e Click event
     * @return {boolean} Event propagates
     */
    submitSend: function ( e ) {
        e.preventDefault();
        $( '#mw-wikilove-success' ).hide();
        $( '#mw-wikilove-dialog' ).find( '.mw-wikilove-error' ).remove();

        // Check for a header if it is required
        if ( currentTypeOrSubtype.fields.indexOf( 'header' ) !== -1 && $( '#mw-wikilove-header' ).val().length === 0 ) {
            $.wikiLove.showAddDetailsError( 'wikilove-err-header' );
            return false;
        }

        // Check for a title if it is required, and otherwise use the header text
        if ( currentTypeOrSubtype.fields.indexOf( 'title' ) !== -1 && $( '#mw-wikilove-title' ).val().length === 0 ) {
            $( '#mw-wikilove-title' ).val( $( '#mw-wikilove-header' ).val() );
        }

        if ( currentTypeOrSubtype.fields.indexOf( 'message' ) !== -1 ) {
            // If there's a signature already in the message, throw an error
            if ( $( '#mw-wikilove-message' ).val().indexOf( '~~~' ) >= 0 ) {
                $.wikiLove.showAddDetailsError( 'wikilove-err-sig' );
                return false;
            }
        }

        // We don't need to do any image validation here since its not actually possible to click
        // Send WikiLove without having a valid image entered.

        const submitData = {
            header: $( '#mw-wikilove-header' ).val(),
            text: $.wikiLove.prepareMsg( currentTypeOrSubtype.text || options.types[ currentTypeId ].text || options.defaultText ),
            message: $( '#mw-wikilove-message' ).val(),
            type: currentTypeId + ( currentSubtypeId !== null ? '-' + currentSubtypeId : '' ),
            extraTags: options.extraTags
        };
        if ( $( '#mw-wikilove-notify-checkbox:checked' ).val() && emailable ) {
            submitData.email = $.wikiLove.prepareMsg( currentTypeOrSubtype.email );
        }
        $.wikiLove.doSend( submitData.header, submitData.text,
            submitData.message, submitData.type, submitData.email, submitData.extraTags );
        return true;
    },

    /**
     * Fires the final AJAX request and then redirects to the talk page where the content is added.
     *
     * @param {string} subject Subject
     * @param {string} wikitext Wikitext
     * @param {string} message Message
     * @param {string} type Type ID
     * @param {string} email E-mail
     * @param {string[]} extraTags Additional tags to apply to the edit
     */
    doSend: function ( subject, wikitext, message, type, email, extraTags ) {
        let targetBaseUrl, currentBaseUrl,
            wikiLoveNumberAttempted = 0,
            wikiLoveNumberPosted = 0;

        // TODO: Use CSS transitions
        // eslint-disable-next-line no-jquery/no-fade
        $( '#mw-wikilove-send-spinner' ).fadeIn( 200 );

        // If the talk page is not a Wikitext page, remove the signature
        if ( mw.config.get( 'wgPageContentModel' ) !== 'wikitext' ) {
            wikitext = wikitext.replace( /\s*~~~~/, '' );
        }

        targets.forEach( ( target ) => {
            const sendData = {
                action: 'wikilove',
                title: 'User:' + target,
                type: type,
                text: wikitext,
                message: message,
                subject: subject,
                tags: extraTags
            };

            if ( email ) {
                sendData.email = email;
            }
            api.postWithToken( 'csrf', sendData )
                .done( ( data ) => {
                    wikiLoveNumberAttempted++;
                    if ( wikiLoveNumberAttempted === targets.length ) {
                        // TODO: Use CSS transitions
                        // eslint-disable-next-line no-jquery/no-fade
                        $( '#mw-wikilove-send-spinner' ).fadeOut( 200 );
                    }

                    if ( data.error !== undefined ) {
                        if ( data.error.info === 'Invalid token' ) {
                            $.wikiLove.showPreviewError( 'wikilove-err-invalid-token' );
                        } else {
                            $.wikiLove.showPreviewError( 'wikilove-err-send-api' );
                        }
                        return;
                    }

                    if ( data.redirect !== undefined ) {
                        wikiLoveNumberPosted++;
                        if ( redirect ) {
                            targetBaseUrl = mw.util.getUrl( data.redirect.pageName );
                            // currentBaseUrl is the current URL minus the hash fragment
                            currentBaseUrl = location.href.split( '#' )[ 0 ];

                            // Set window location to user talk page URL + WikiLove anchor hash.
                            // Unfortunately, in the most common scenario (starting from the user talk
                            // page) this won't reload the page since the browser will simply try to jump
                            // to the anchor within the existing page (which doesn't exist). This does,
                            // however, prepare us for the subsequent reload, making sure that the user is
                            // directed to the WikiLove message instead of just being left at the top of
                            // the page. In the case that we are starting from a different page, this sends
                            // the user immediately to the new WikiLove message on the user talk page.
                            location.href = targetBaseUrl + '#' + data.redirect.fragment; // data.redirect.fragment is already encoded

                            // If we were already on the user talk page, then reload the page so that the
                            // new WikiLove message is displayed.
                            // @todo: an expandUrl() would be very nice indeed!
                            if (
                                mw.config.get( 'wgServer' ) + targetBaseUrl === currentBaseUrl ||
                                // Compatibility with protocol-relative URLs in MediaWiki 1.18+
                                location.protocol + mw.config.get( 'wgServer' ) + targetBaseUrl === currentBaseUrl
                            ) {
                                location.reload();
                            }
                        } else {
                            $.wikiLove.showSuccessMsg( mw.msg( 'wikilove-success-number', wikiLoveNumberPosted ) );
                            // If there were no errors, close the dialog and reset WikiLove
                            if ( wikiLoveNumberPosted === targets.length ) {
                                setTimeout( () => {
                                    this.reset();
                                }, 1000 );
                            }
                        }
                    } else { // API did not return appropriate information
                        $.wikiLove.showPreviewError( 'wikilove-err-send-api' );
                    }
                } )
                .fail( () => {
                    $.wikiLove.showPreviewError( 'wikilove-err-send-api' );
                    wikiLoveNumberAttempted++;
                    if ( wikiLoveNumberAttempted === targets.length ) {
                        // TODO: Use CSS transitions
                        // eslint-disable-next-line no-jquery/no-fade
                        $( '#mw-wikilove-send-spinner' ).fadeOut( 200 );
                    }
                } );
        } );
    },

    /**
     * Hides the WikiLove overlay. The overlay is retained in the DOM for future clicks.
     */
    reset: function () {
        overlayContainer.style.display = 'none';
    },

    /**
     * This function is called if the gallery is an array of images. It retrieves the image
     * thumbnails from the API, and constructs a thumbnail gallery with them.
     */
    showGallery: function () {
        let i, id, index, loadingIndex, galleryNumber, $img;
        const titles = [];
        const imageList = currentTypeOrSubtype.gallery.imageList.slice();

        $( '#mw-wikilove-gallery-content' ).empty();
        gallery = {};
        // TODO: Use CSS transitions
        // eslint-disable-next-line no-jquery/no-fade
        $( '#mw-wikilove-gallery-spinner' ).fadeIn( 200 );
        $( '#mw-wikilove-gallery-error' ).hide();

        if (
            currentTypeOrSubtype.gallery.number === undefined ||
            currentTypeOrSubtype.gallery.number <= 0
        ) {
            currentTypeOrSubtype.gallery.number = currentTypeOrSubtype.gallery.imageList.length;
        }

        for ( i = 0; i < currentTypeOrSubtype.gallery.number; i++ ) {
            // get a random image from imageList and add it to the list of titles to be retrieved
            id = Math.floor( Math.random() * imageList.length );
            titles.push( $.wikiLove.normalizeFilename( imageList[ id ] ) );

            // remove the randomly selected image from imageList so that it can't be added twice
            imageList.splice( id, 1 );
        }

        index = 0;
        const loadingType = currentTypeOrSubtype;
        loadingIndex = 0;
        api.post( {
            formatversion: 2,
            action: 'query',
            prop: 'imageinfo',
            iiprop: 'mime|url',
            titles: titles,
            iiurlwidth: currentTypeOrSubtype.gallery.width,
            iiurlheight: currentTypeOrSubtype.gallery.height
        } )
            .done( ( data ) => {
                if ( !data || !data.query || !data.query.pages ) {
                    $( '#mw-wikilove-gallery-error' ).show();
                    // TODO: Use CSS transitions
                    // eslint-disable-next-line no-jquery/no-fade
                    $( '#mw-wikilove-gallery-spinner' ).fadeOut( 200 );
                    return;
                }

                if ( loadingType !== currentTypeOrSubtype ) {
                    return;
                }
                galleryNumber = currentTypeOrSubtype.gallery.number;

                data.query.pages.forEach( ( page ) => {
                    if ( page.imageinfo && page.imageinfo.length ) {
                        // build an image tag with the correct url
                        $img = $( '<img>' )
                            .attr( 'src', page.imageinfo[ 0 ].thumburl )
                            .hide()
                            .on( 'load', function () {
                                $( this ).css( 'display', 'inline-block' );
                                loadingIndex++;
                                if ( loadingIndex >= galleryNumber ) {
                                    // TODO: Use CSS transitions
                                    // eslint-disable-next-line no-jquery/no-fade
                                    $( '#mw-wikilove-gallery-spinner' ).fadeOut( 200 );
                                }
                            } );
                        $( '#mw-wikilove-gallery-content' ).append(
                            $( '<a>' )
                                .attr( 'href', '#' )
                                .attr( 'id', 'mw-wikilove-gallery-img-' + index )
                                .append( $img )
                                .on( 'click', function ( e ) {
                                    e.preventDefault();
                                    $( '#mw-wikilove-gallery a' ).removeClass( 'selected' );
                                    $( this ).addClass( 'selected' );
                                    $( '#mw-wikilove-image' ).val( gallery[ $( this ).attr( 'id' ) ] );
                                } )
                        );
                        gallery[ 'mw-wikilove-gallery-img-' + index ] = page.title;
                        index++;
                    }
                } );
                // Pre-select first image
                /* $( '#mw-wikilove-gallery-img-0 img' ).trigger( 'click' ); */
            } )
            .fail( () => {
                $( '#mw-wikilove-gallery-error' ).show();
                // TODO: Use CSS transitions
                // eslint-disable-next-line no-jquery/no-fade
                $( '#mw-wikilove-gallery-spinner' ).fadeOut( 200 );
            } );
    },

    /**
     * Init function which is called upon page load. Binds the WikiLove icon to opening the dialog.
     */
    init: function () {
        let $wikiLoveLink = $( [] );
        options = $.wikiLoveOptions;

        if ( $( '#ca-wikilove' ).length ) {
            $wikiLoveLink = $( '#ca-wikilove' ).find( 'a' );
        } else { // legacy skins
            $wikiLoveLink = $( '#topbar a:contains(' + mw.msg( 'wikilove-tab-text' ) + ')' );
        }
        $wikiLoveLink.off( 'click' );
        $wikiLoveLink.on( 'click', ( e ) => {
            e.preventDefault();
            $.wikiLove.openDialog();
        } );
    }
};