wikimedia/mediawiki-extensions-Translate

View on GitHub
resources/js/ext.translate.special.managetranslatorsandbox.js

Summary

Maintainability
F
5 days
Test Coverage
/*!
 * JS for special page.
 * @author Niklas Laxström
 * @author Sucheta Ghoshal
 * @author Amir E. Aharoni
 * @author Pau Giner
 * @license GPL-2.0-or-later
 */

( function () {
    'use strict';

    var delay;

    /**
     * A callback for sorting translations.
     *
     * @param {Object} translationA Object loaded from translation stash
     * @param {Object} translationB Object loaded from translation stash
     * @return {number} String comparison of language codes
     */
    function sortTranslationsByLanguage( translationA, translationB ) {
        var a = translationA.title.split( '/' ).pop(),
            b = translationB.title.split( '/' ).pop();

        return a.localeCompare( b );
    }

    function doApiAction( options ) {
        var api = new mw.Api(),
            optionsWithDefaults = $.extend( {}, { action: 'translatesandbox' }, options );

        return api.postWithToken( 'csrf', optionsWithDefaults ).promise();
    }

    function removeSelectedRequests() {
        var $selectedRequests = $( '.request-selector:checked' );

        var $nextRequest = $selectedRequests
            .first() // First selected request
            .closest( '.request' ) // The request corresponds that checkbox
            .prevAll( ':not(.hide)' ) // Go back till a non-hidden request
            .first(); // The above selector gives list from bottom to top. Select the bottom one.

        $selectedRequests.closest( '.request' ).remove();

        updateRequestCount();

        if ( !$nextRequest.length ) {
            // If there's no request above the first checked request,
            // try to get the first request in the column
            $nextRequest = $( '.requests .request:not(.hide)' ).first();
        }

        if ( $nextRequest.length ) {
            $nextRequest.trigger( 'click' );
            updateSelectedIndicator( 1 );
        } else {
            updateSelectedIndicator( 0 );
        }
    }

    /**
     * Display the request details when user clicks on a request item
     *
     * @param {Object} request The request data set from backend on request items
     */
    function displayRequestDetails( request ) {
        var $reminderStatus = $( '<span>' ).addClass( 'reminder-status' ),
            $detailsPane = $( '.details.pane' );
        if ( request.reminderscount ) {
            var agoText = moment.isMoment( request.lastreminder ) ? moment( request.lastreminder ).fromNow() : request.lastreminder;
            $reminderStatus.text( mw.msg(
                'tsb-reminder-sent',
                request.reminderscount,
                agoText
            ) );
        }

        $detailsPane.empty().append(
            $( '<div>' )
                .addClass( 'tsb-header row' )
                .text( request.username ),
            $( '<div>' )
                .addClass( 'reminder-email row' )
                .append(
                    $( '<span>' )
                        .attr( { dir: 'ltr' } )
                        .text( request.email ),
                    $( '<a>' )
                        .prop( 'href', '#' )
                        .addClass( 'send-reminder link' )
                        .text( mw.msg( 'tsb-reminder-link-text' ) )
                        .on( 'click', function ( e ) {
                            e.preventDefault();

                            $reminderStatus
                                .text( mw.msg( 'tsb-reminder-sending' ) );

                            doApiAction( {
                                do: 'remind',
                                userid: request.userid
                            } ).done( function () {
                                request.lastreminder = moment();
                                request.reminderscount++;
                                $reminderStatus.text( mw.msg( 'tsb-reminder-sent-new' ) );
                            } ).fail( function () {
                                $reminderStatus.text( mw.msg( 'tsb-reminder-failed' ) );
                            } );
                        } ),
                    $reminderStatus
                ),
            $( '<div>' )
                .addClass( 'languages row autonym' ),
            $( '<div>' )
                .addClass( 'signup-comment row' ),
            $( '<div>' )
                .addClass( 'actions row' )
                .append(
                    $( '<button>' )
                        .addClass( 'accept mw-ui-button mw-ui-progressive' )
                        .text( mw.msg( 'tsb-accept-button-label' ) )
                        .on( 'click', function () {
                            mw.notify( mw.msg( 'tsb-accept-confirmation', 1 ) );

                            window.tsbUpdatingUsers = true;

                            doApiAction( {
                                userid: request.userid,
                                do: 'promote'
                            } ).done( function () {
                                removeSelectedRequests();

                                window.tsbUpdatingUsers = false;
                            } );
                        } ),
                    $( '<button>' )
                        .addClass( 'reject mw-ui-button mw-ui-destructive' )
                        .text( mw.msg( 'tsb-reject-button-label' ) )
                        .on( 'click', function () {
                            mw.notify( mw.msg( 'tsb-reject-confirmation', 1 ) );

                            window.tsbUpdatingUsers = true;

                            doApiAction( {
                                userid: request.userid,
                                do: 'delete'
                            } ).done( function () {
                                removeSelectedRequests();

                                window.tsbUpdatingUsers = false;
                            } );
                        } )
                ),
            $( '<div>' )
                .addClass( 'translations' )
        );

        if ( request.languagepreferences ) {
            if ( request.languagepreferences.languages ) {
                request.languagepreferences.languages.forEach( function ( language ) {
                    $detailsPane.find( '.languages' ).append(
                        $( '<span>' )
                            .prop( {
                                dir: $.uls.data.getDir( language ),
                                lang: language
                            } )
                            .text( $.uls.data.getAutonym( language ) )
                    );
                } );
            }

            if ( request.languagepreferences.comment ) {
                $detailsPane.find( '.signup-comment' ).append(
                    $( '<div>' )
                        .addClass( 'signup-comment-label' )
                        .text( mw.msg( 'tsb-user-posted-a-comment' ) ),
                    $( '<div>' )
                        .addClass( 'signup-comment-text' )
                        .text( request.languagepreferences.comment )
                );
            }
        }

        getUserTranslations( request.username ).done( showTranslations );
    }

    /**
     * Get the current users translations.
     *
     * @param {string} user User name
     * @return {jQuery.Promise}
     */
    function getUserTranslations( user ) {
        var api = new mw.Api();

        return api.postWithToken( 'csrf', {
            action: 'translationstash',
            subaction: 'query',
            username: user
        } );
    }

    function showTranslations( translations ) {
        var $target = $( '.translations' );

        $target.empty();

        // Display a message if the user didn't make any translations
        if ( !translations.translationstash.translations.length ) {
            $target.append(
                $( '<div>' )
                    .addClass( 'tsb-details-no-translations' )
                    .text( mw.msg( 'tsb-didnt-make-any-translations' ) )
            );

            return;
        }

        var gender = $( '.requests-list .request.selected' ).data( 'data' ).gender;
        $target.append(
            $( '<div>' )
                .addClass( 'row title' )
                .append(
                    $( '<div>' )
                        .text( mw.msg( 'tsb-translations-source' ) )
                        .addClass( 'four columns' ),
                    $( '<div>' )
                        .text( mw.msg( 'tsb-translations-user', gender ) )
                        .addClass( 'four columns' ),
                    $( '<div>' )
                        .text( mw.msg( 'tsb-translations-current' ) )
                        .addClass( 'four columns' )
                )
        );

        translations.translationstash.translations.sort( sortTranslationsByLanguage );
        translations.translationstash.translations.forEach( function ( translation ) {
            showTranslation( translation );
        } );
    }

    function showTranslation( translation ) {
        var $target = $( '.translations' ),
            translationLang = translation.title.split( '/' ).pop();

        $target.append( $( '<div>' )
            .addClass( 'row' )
            .append(
                $( '<div>' )
                    .addClass( 'four columns source' )
                    .text( translation.definition ),
                $( '<div>' )
                    .addClass( 'four columns translation' )
                    .append(
                        $( '<div>' ).text( translation.translation )
                            .prop( {
                                dir: $.uls.data.getDir( translationLang ),
                                lang: translationLang
                            } ),
                        $( '<div>' )
                            .addClass( 'info autonym' )
                            .prop( {
                                dir: $.uls.data.getDir( translationLang ),
                                lang: translationLang
                            } )
                            .text(
                                $.uls.data.getAutonym( translationLang )
                            )
                    ),
                $( '<div>' )
                    .addClass( 'four columns comparison' )
                    .append(
                        $( '<div>' ).text( translation.comparison || '' ),
                        $( '<div>' )
                            .addClass( 'info' )
                            .text( translation.title )
                    )
            )
        );
    }

    /**
     * Display when multiple requests are checked.
     */
    function displayOnMultipleSelection() {
        var selectedUserIDs = $( '.request-selector:checked' ).map( function ( i, checkedBox ) {
            return $( checkedBox ).parents( 'div.request' ).data( 'data' ).userid;
        } ).toArray();

        $( '.details.pane' ).empty().append(
            $( '<div>' )
                .addClass( 'tsb-header row' ),
            $( '<div>' )
                .addClass( 'actions row' )
                .append(
                    $( '<button>' )
                        .addClass( 'accept-all mw-ui-button mw-ui-progressive' )
                        .text( mw.msg( 'tsb-accept-all-button-label' ) )
                        .on( 'click', function () {
                            mw.notify( mw.msg( 'tsb-accept-confirmation', selectedUserIDs.length ) );

                            window.tsbUpdatingUsers = true;

                            doApiAction( {
                                userid: selectedUserIDs,
                                do: 'promote'
                            } ).done( function () {
                                removeSelectedRequests();

                                window.tsbUpdatingUsers = false;
                            } );
                        } ),
                    $( '<button>' )
                        .addClass( 'reject-all mw-ui-button mw-ui-destructive' )
                        .text( mw.msg( 'tsb-reject-all-button-label' ) )
                        .on( 'click', function () {
                            mw.notify( mw.msg( 'tsb-reject-confirmation', selectedUserIDs.length ) );

                            window.tsbUpdatingUsers = true;

                            doApiAction( {
                                userid: selectedUserIDs,
                                do: 'delete'
                            } ).done( function () {
                                removeSelectedRequests();

                                window.tsbUpdatingUsers = false;
                            } );
                        } )
                )
        );
    }

    /**
     * Updates the counter of the selected users.
     *
     * @param {number} count The number of selected users
     */
    function updateSelectedIndicator( count ) {
        var text = mw.msg( 'tsb-selected-count', mw.language.convertNumber( count ) );

        $( '.requests.pane .request-footer .selected-counter' ).text( text );
        if ( count > 1 ) {
            $( '.details.pane .tsb-header' ).text( text );
        }
    }

    /**
     * Returns older requests with the same number of translations.
     *
     * @return {jQuery} Older requests
     */
    function getOlderRequests() {
        var $lastSelectedRequest = $( '.row.request.selected' ).last(),
            currentTranslationCount;

        if ( $lastSelectedRequest.length === 0 ) {
            return [];
        }

        currentTranslationCount = $lastSelectedRequest.data( 'data' ).translations;
        return $lastSelectedRequest.nextAll( ':not(.hide)' ).filter( function () {
            return ( $( this ).data( 'data' ).translations === currentTranslationCount );
        } );
    }

    /**
     * Updates the number of older requests with the same number
     * of translations at the link in the bottom of the requests row
     * or hides that link if there are no such requests.
     */
    function indicateOlderRequests() {
        var $olderRequests = getOlderRequests(),
            $olderRequestsIndicator = $( '.older-requests-indicator' );

        var oldRequestsCount = $olderRequests.length;
        var oldRequestsCountString = mw.language.convertNumber( oldRequestsCount );

        if ( oldRequestsCount ) {
            $olderRequestsIndicator
                .text( mw.msg( 'tsb-older-requests', oldRequestsCountString ) )
                .removeClass( 'hide' );
        } else {
            $olderRequestsIndicator
                .addClass( 'hide' );
        }
    }

    /**
     * Updates the number of requests.
     */
    function updateRequestCount() {
        var $requests = $( '.requests-list .request' ),
            visibleRequestsCount = $requests.filter( ':not(.hide)' ).length;

        $( '.request-count' ).text(
            mw.msg( 'tsb-request-count', mw.language.convertNumber( visibleRequestsCount ) )
        );

        if ( $requests.length === 0 ) {
            $( '.details.pane' )
                .empty()
                .append(
                    $( '<div>' )
                        .addClass( 'tsb-header row' )
                        .text( mw.msg( 'tsb-no-requests-from-new-users' ) )
                );
        }
    }

    /**
     * Sets the height of the panes to the window height.
     */
    function setPanesHeight() {
        var $detailsPane = $( '.details.pane' ),
            $requestsPane = $( '.requests.pane' ),
            detailsHeight = $( window ).height() - $detailsPane.offset().top,
            requestsHeight = detailsHeight -
                $requestsPane.find( '.request-footer' ).height() -
                $requestsPane.find( '.request-header' ).height();

        $detailsPane.css( 'max-height', detailsHeight );
        $requestsPane.find( '.requests-list' ).css( 'max-height', requestsHeight );
    }

    function selectAllRequests() {
        var $requestCheckboxes = $( '.request-selector' ),
            $detailsPane = $( '.details.pane' ),
            $selectAll = $( '.request-selector-all' ),
            $requestRows = $( '.requests .request' ),
            selectAllChecked = $selectAll.prop( 'checked' ),
            $visibleRows = $requestRows.not( '.hide' );

        $visibleRows.each( function ( index, row ) {
            $( row ).find( '.request-selector' ).prop( {
                checked: selectAllChecked,
                disabled: false
            } );
        } );

        var selectedCount;
        if ( selectAllChecked ) {
            displayOnMultipleSelection();
            $visibleRows.addClass( 'selected' );
            selectedCount = $requestCheckboxes.filter( ':checked' ).length;
        } else {
            $detailsPane.empty();
            $requestRows.removeClass( 'selected' );
            selectedCount = 0;
        }

        updateSelectedIndicator( selectedCount );
        indicateOlderRequests();
    }

    /**
     * Handle click on request row
     *
     * @param {jQuery.Event} e
     */
    function onSelectRequest( e ) {
        var $requestRow = $( e.target ).closest( '.request' ),
            $requestRows = $( '.requests .request' ),
            $selectAll = $( '.request-selector-all' );

        displayRequestDetails( $requestRow.data( 'data' ) );

        // Clicking a row makes only that row selected and unselects all other rows
        $requestRows.each( function ( i, row ) {
            var $row = $( row );

            if ( row === $requestRow[ 0 ] ) {
                $row.addClass( 'selected' )
                    .find( '.request-selector' ).prop( {
                        checked: true,
                        disabled: true
                    } );
            } else {
                $row.removeClass( 'selected' )
                    .find( '.request-selector' ).prop( {
                        checked: false,
                        disabled: false
                    } );
            }
        } );

        $selectAll.prop( 'indeterminate', true );

        updateSelectedIndicator( 1 );
        indicateOlderRequests();
    }

    /**
     * Event handler for request checkbox selection.
     *
     * @param {jQuery.Event} e
     */
    function requestSelectHandler( e ) {
        var request = e.target,
            $detailsPane = $( '.details.pane' ),
            $requestCheckboxes = $( '.request-selector' ),
            $selectAll = $( '.request-selector-all' ),
            $thisRequestRow = $( request ).parents( 'div.request' );

        // Uncheck the rows that were selected by clicking the row
        $requestCheckboxes.filter( ':disabled' ).prop( 'disabled', false );

        $thisRequestRow.toggleClass( 'selected', request.checked );

        var $checkedBoxes = $requestCheckboxes.filter( ':checked' );
        var checkedCount = $checkedBoxes.length;

        if ( checkedCount === $requestCheckboxes.length ) {
            // All boxes are selected
            $selectAll.prop( {
                checked: true,
                indeterminate: false
            } );

            displayOnMultipleSelection();
        } else if ( checkedCount === 0 ) {
            // No boxes are selected
            $selectAll.prop( {
                checked: false,
                indeterminate: false
            } );

            $detailsPane.empty();
        } else if ( checkedCount === 1 ) {
            $selectAll.prop( {
                checked: false,
                indeterminate: true
            } );

            $checkedBoxes.prop( 'disabled', true );

            // Here we know that only one checkbox is selected,
            // so it's OK to query the data from it
            displayRequestDetails( $checkedBoxes.parents( 'div.request' ).data( 'data' ) );
        } else {
            $selectAll.prop( {
                checked: false,
                indeterminate: true
            } );

            displayOnMultipleSelection();
        }

        updateSelectedIndicator( checkedCount );
        indicateOlderRequests();

        e.stopPropagation();
    }

    /**
     * Old request click handler.
     *
     * @param {jQuery.Event} e
     */
    function oldRequestSelector( e ) {
        e.preventDefault();

        getOlderRequests().each( function ( index, request ) {
            $( request ).find( '.request-selector' )
                .prop( 'checked', true ) // Otherwise the state doesn't actually change
                .trigger( 'change' );
        } );
    }

    // ======================================
    // LanguageFilter plugin
    // ======================================
    function LanguageFilter( element ) {
        this.$selector = $( element );
        this.init();
    }

    LanguageFilter.prototype.init = function () {
        var languageFilter = this;

        var $clearButton = $( '<button>' )
            .addClass( 'clear-language-selector hide' )
            .text( '×' );

        languageFilter.$selector.after( $clearButton );
        // Activate language selector
        languageFilter.$selector.uls( {
            onSelect: function ( language ) {
                languageFilter.$selector
                    .removeClass( 'unselected' )
                    .addClass( 'selected autonym' )
                    .prop( {
                        dir: $.uls.data.getDir( language ),
                        lang: language
                    } )
                    .text( $.uls.data.getAutonym( language ) );

                languageFilter.filter( language );
                $clearButton.removeClass( 'hide' );
                indicateOlderRequests();
            },
            ulsPurpose: 'translate-special-managetranslatorsandbox',
            quickList: mw.uls.getFrequentLanguageList
        } );

        $clearButton.on( 'click', function () {
            var userLang = mw.config.get( 'wgUserLanguage' );

            languageFilter.$selector
                .removeClass( 'selected autonym' )
                .prop( {
                    dir: $.uls.data.getDir( userLang ),
                    lang: userLang
                } )
                .addClass( 'unselected' )
                .text( mw.msg( 'tsb-all-languages-button-label' ) );

            languageFilter.filter();
            $clearButton.addClass( 'hide' );
        } );
    };

    /**
     * Filter the requests by language.
     *
     * @param {string} [language] Language code
     */
    LanguageFilter.prototype.filter = function ( language ) {
        var $requests = $( '.request' );

        $requests.each( function ( index, request ) {
            var $request = $( request ),
                requestData = $request.data( 'data' );

            if ( !language ||
                ( requestData.languagepreferences &&
                    requestData.languagepreferences.languages &&
                    requestData.languagepreferences.languages.indexOf( language ) > -1 )
            ) {
                // Found language
                $request.removeClass( 'hide' );
            } else {
                $request.addClass( 'hide' );
            }
        } );

        updateAfterFiltering();
    };

    $.fn.languageFilter = function () {
        return this.each( function () {
            if ( !$.data( this, 'LanguageFilter' ) ) {
                $.data( this, 'LanguageFilter', new LanguageFilter( this ) );
            }
        } );
    };

    // ======================================
    // TranslatorSearch plugin
    // ======================================
    function TranslatorSearch( element ) {
        this.$search = $( element );
        this.init();
    }

    TranslatorSearch.prototype.init = function () {
        this.$search.on( 'search keyup', this.keyup.bind( this ) );
    };

    TranslatorSearch.prototype.keyup = function () {
        var translatorSearch = this;

        // Respond to the keypress events after a small timeout to avoid freeze when typed fast
        delay( function () {
            var query = translatorSearch.$search.val().trim().toLowerCase();
            translatorSearch.filter( query );
        }, 300 );
    };

    TranslatorSearch.prototype.filter = function ( query ) {
        var $requests = $( '.request' );

        $requests.each( function ( index, request ) {
            var $request = $( request ),
                requestData = $request.data( 'data' );

            if ( query.length === 0 ||
                requestData.username.toLowerCase().indexOf( query ) === 0 ||
                requestData.email.toLowerCase().indexOf( query ) === 0
            ) {
                $request.removeClass( 'hide' );
            } else {
                $request.addClass( 'hide' );
            }
        } );

        updateAfterFiltering();
    };

    function updateAfterFiltering() {
        var $firstVisibleUser = $( '.request:not(.hide)' ).first();

        if ( $firstVisibleUser.length ) {
            $firstVisibleUser.trigger( 'click' );
        } else {
            $( '.details.pane' ).empty();
            var $selectedRequests = $( '.request-selector:checked' );
            $selectedRequests.closest( '.request' ).removeClass( 'selected' );
            $selectedRequests.prop( {
                checked: false,
                disabled: false
            } );

            updateSelectedIndicator( 0 );
        }

        updateRequestCount();
    }

    $.fn.translatorSearch = function () {
        return this.each( function () {
            if ( !$.data( this, 'TranslatorSearch' ) ) {
                $.data( this, 'TranslatorSearch', new TranslatorSearch( this ) );
            }
        } );
    };

    delay = ( function () {
        var timer = 0;

        return function ( callback, milliseconds ) {
            clearTimeout( timer );
            timer = setTimeout( callback, milliseconds );
        };
    }() );

    $( function () {
        var $requestCheckboxes = $( '.request-selector' ),
            $selectAll = $( '.request-selector-all' ),
            $requestRows = $( '.requests .request' );

        // Delay so we get the correct height on page load
        window.setTimeout( setPanesHeight, 0 );
        $( window ).on( 'resize', setPanesHeight );

        $( '.request-filter-box' ).translatorSearch();
        $( '.language-selector' ).languageFilter();

        // Handle clicks for the 'Select all' checkbox
        $selectAll.on( 'click', selectAllRequests );

        // Handle clicks on request checkboxes.
        $requestCheckboxes.on( 'click change', requestSelectHandler );

        // Handle clicks on request rows.
        $requestRows.on( 'click', onSelectRequest );

        $( '.older-requests-indicator' ).on( 'click', oldRequestSelector );

        if ( $requestRows.length ) {
            $requestRows.first().trigger( 'click' );
        }

        updateRequestCount();
    } );
}() );