wikimedia/mediawiki-extensions-Translate

View on GitHub
resources/src/ext.translate.special.languagestats/index.js

Summary

Maintainability
D
1 day
Test Coverage
/*!
 * Collapsing script for Special:LanguageStats in MediaWiki Extension:Translate
 * @author Krinkle <krinklemail (at) gmail (dot) com>
 * @author Niklas Laxström
 * @license GPL-2.0-or-later, CC-BY-SA-3.0
 */

( function () {
    'use strict';

    var $columns;

    /**
     * Add css class to every other visible row.
     * It's not possible to do zebra colors with CSS only if there are hidden rows.
     *
     * @param {jQuery} $table
     */
    function doZebra( $table ) {
        $table.find( 'tr:visible:odd' ).toggleClass( 'tux-statstable-even', false );
        $table.find( 'tr:visible:even' ).toggleClass( 'tux-statstable-even', true );
    }

    function addExpanders( $table ) {
        var $metaRows = $( 'tr.AggregateMessageGroup', $table );

        // Quick return
        if ( !$metaRows.length ) {
            return;
        }

        $metaRows.each( function () {
            var $parent = $( this ),
                thisGroupId = $parent.attr( 'data-groupid' ),
                $children = $( 'tr[data-parentgroup="' + thisGroupId + '"]', $table );

            // Only do the collapse stuff if this Meta-group actually has children on this page
            if ( !$children.length ) {
                return;
            }

            // Build toggle link
            var $toggler = $( '<span>' ).addClass( 'groupexpander collapsed' )
                .append(
                    '[',
                    $( '<a>' )
                        .attr( 'href', '#' )
                        .text( mw.msg( 'translate-langstats-expand' ) ),
                    ']'
                )
                .on( 'click', function ( e ) {
                    var $el = $( this );
                    // Switch the state and toggle the rows
                    if ( $el.hasClass( 'collapsed' ) ) {
                        $children.removeClass( 'statstable-hide' ).trigger( 'show' );
                        doZebra( $table );
                        $el.removeClass( 'collapsed' ).addClass( 'expanded' );
                        $el.find( '> a' ).text( mw.msg( 'translate-langstats-collapse' ) );
                    } else {
                        $children.addClass( 'statstable-hide' ).trigger( 'hide' );
                        doZebra( $table );
                        $el.addClass( 'collapsed' ).removeClass( 'expanded' );
                        $el.find( '> a' ).text( mw.msg( 'translate-langstats-expand' ) );
                    }

                    e.preventDefault();
                } );

            // Add the toggle link to the first cell of the meta group table-row
            $parent.find( ' > td' ).first().append( $toggler );

            // Handle hide/show recursively, so that collapsing parent group
            // hides all sub groups regardless of nesting level
            $parent.on( 'hide show', function ( event ) {
                // Reuse $toggle, $parent and $children from parent scope
                if ( $toggler.hasClass( 'expanded' ) ) {
                    $children.trigger( event.type )[ event.type ]();
                }
            } );
        } );

        // Create, bind and append the toggle-all button
        var $allChildRows = $( 'tr[data-parentgroup]', $table );
        var $allTogglesCache = null;
        var $toggleAllButton = $( '<span>' ).addClass( 'collapsed' )
            .append(
                '[',
                $( '<a>' )
                    .attr( 'href', '#' )
                    .text( mw.msg( 'translate-langstats-expandall' ) ),
                ']'
            )
            .on( 'click', function ( e ) {
                var $el = $( this ),
                    $allToggles = $allTogglesCache || $( '.groupexpander', $table );

                // Switch the state and toggle the rows
                // and update the local toggles too
                if ( $el.hasClass( 'collapsed' ) ) {
                    $allChildRows.removeClass( 'statstable-hide' );
                    $el.add( $allToggles ).removeClass( 'collapsed' ).addClass( 'expanded' );
                    $el.find( '> a' ).text( mw.msg( 'translate-langstats-collapseall' ) );
                    $allToggles.find( '> a' ).text( mw.msg( 'translate-langstats-collapse' ) );
                } else {
                    $allChildRows.addClass( 'statstable-hide' );
                    $el.add( $allToggles ).addClass( 'collapsed' ).removeClass( 'expanded' );
                    $el.find( '> a' ).text( mw.msg( 'translate-langstats-expandall' ) );
                    $allToggles.find( '> a' ).text( mw.msg( 'translate-langstats-expand' ) );
                }

                doZebra( $table );
                e.preventDefault();
            } );

        // Initially hide them
        $allChildRows.addClass( 'statstable-hide' );
        doZebra( $table );

        // Add the toggle-all button above the table
        $( '<p>' ).addClass( 'groupexpander-all' ).append( $toggleAllButton ).insertBefore( $table );
    }

    function applySorting( $table ) {
        var sort = {},
            re = /#sortable:(\d+)=(asc|desc)/,
            match = re.exec( location.hash );

        if ( match ) {
            var index = parseInt( match[ 1 ], 10 );
            sort[ index ] = match[ 2 ];
        }
        $table.tablesorter( { sortList: [ sort ] } );

        $table.on( 'sortEnd.tablesorter', function () {
            $table.find( '.headerSortDown, .headerSortUp' ).each( function () {
                var headerIndex = $table.find( 'th' ).index( $( this ) ),
                    dir = $( this ).hasClass( 'headerSortUp' ) ? 'asc' : 'desc';
                location.hash = 'sortable:' + headerIndex + '=' + dir;

                doZebra( $table );
            } );
        } );
    }

    function narrowTable( $table, enable ) {
        var labelColumnCount = 1,
            // 0-indexed
            defaultValueColumn = 2;

        if ( $columns === undefined ) {
            $columns = $table.find( 'thead > tr > th ' ).map( function ( index, value ) {
                return value.textContent;
            } );
        }

        var $select = makeValueColumnSelector( $columns, labelColumnCount, defaultValueColumn );
        // Prevent table sorter from making the select inaccessible
        $select.on( 'mousedown click', function ( e ) {
            e.stopPropagation();
        } ).on( 'change', function () {
            showValueColumn( $table, $select, labelColumnCount );
        } );

        if ( enable ) {
            showValueColumn( $table, $select, labelColumnCount );
        } else {
            // Restore original headings
            $table.find( 'thead > tr > th' ).map( function ( index ) {
                return $( this ).text( $columns[ index ] );
            } );
            $table.find( 'tr > *' ).removeClass( 'statstable-hide' );
        }

    }

    function makeValueColumnSelector( headings, skip, def ) {
        var $select = $( '<select>' );

        for ( var i = skip; i < headings.length; i++ ) {
            $( '<option>' )
                .text( headings[ i ] )
                .val( i )
                .prop( 'selected', i === def )
                .appendTo( $select );
        }

        return $select;
    }

    function showValueColumn( $table, $select, skip ) {
        var index = parseInt( $select.val(), 10 );
        var cssQuery = 'th:nth-child(_)'.replace( '_', index + 1 );
        $table.find( cssQuery ).html( $select );

        for ( var i = 0; i < $select.children().length; i++ ) {
            cssQuery = 'tr > *:nth-child(_)'.replace( '_', i + skip + 1 );
            $table.find( cssQuery ).toggleClass( 'statstable-hide', i + skip !== index );
        }
    }

    function activateEntitySelector( $group, $messagePrefix ) {
        // hide the message group and prefix selector
        var $groupContainer = $( '.message-group-selector' );

        // Change the label, and update the for attribute, and remove the click handler
        // which causes the entity selector to become un-responsive when triggered
        $groupContainer
            .find( 'label' )
            .text( mw.msg( 'translate-mgs-group-or-prefix' ) )
            .attr( 'for', 'mw-entity-selector-input' )
            .off( 'click' );

        // Determine what value was set, and set it on the entity selector
        var selectedGroup = $group.find( 'select option:selected' ).text();

        // load the entity selector and set the value
        var entitySelector = getEntitySelector( onEntitySelect );
        if ( selectedGroup ) {
            entitySelector.setValue( selectedGroup );
        } else {
            var selectedMessage = $messagePrefix.val();
            if ( selectedMessage ) {
                entitySelector.setValue( selectedMessage );
            }
        }

        $group.addClass( 'hidden' );
        $group.after( entitySelector.$element );
    }

    function onEntitySelect( selectedItem ) {
        if ( selectedItem.type === 'group' ) {
            $( 'select[name="group"]' ).val( selectedItem.data );
            $( 'input[name="messages"]' ).val( '' );
        } else {
            $( 'input[name="messages"]' ).val( selectedItem.data );
            $( 'select[name="group"]' ).val( '' );
        }
    }

    function onSubmit() {
        var selectedGroupName = $( 'select[name="group"]' ).find( 'option:selected' ).text();
        var selectedMessagePrefix = $( 'input[name="messages"]' ).val();
        var currentVal = $( '.tes-entity-selector' ).find( 'input[type="text"]' ).val();

        // Check if the user has selected an invalid entity.
        if ( currentVal !== selectedGroupName && currentVal !== selectedMessagePrefix ) {
            mw.notify(
                mw.msg( 'translate-mgs-invalid-entity' ),
                {
                    type: 'error',
                    tag: 'invalid-selection'
                }
            );
            return false;
        }
    }

    function getEntitySelector( onSelect ) {
        var EntitySelector = require( 'ext.translate.entity.selector' );
        return new EntitySelector( {
            onSelect: onSelect,
            entityType: [ 'groups', 'messages' ],
            inputId: 'mw-entity-selector-input'
        } );
    }

    $( function () {
        var $table = $( '.statstable' );

        activateEntitySelector( $( '#group' ), $( 'input[name="messages"]' ) );

        // Sometimes the table is not present on the page
        if ( !$table.length ) {
            return;
        }

        // Calculate absolute minimum table width
        if ( window.ResizeObserver ) {
            $table.css( 'max-width', '1px' );
        }

        applySorting( $table );
        addExpanders( $table );

        $( '#mw-message-group-stats-form' ).on( 'submit', onSubmit );

        if ( !window.ResizeObserver ) {
            return;
        }

        var minimumTableWidth;
        // Hopefully previous stuff have time to render by now to have accurate picture of the width
        ( window.requestAnimationFrame || setTimeout )( function () {
            minimumTableWidth = $table.outerWidth();
            $table.css( 'max-width', '' );
        } );

        var isNarrowMode;
        new ResizeObserver( function ( entries ) {
            var shouldCollapse = entries[ 0 ].contentRect.width < minimumTableWidth;
            // Some fudge to avoid flapping
            var shouldExpand = entries[ 0 ].contentRect.width - 20 > minimumTableWidth;

            var newMode;
            if ( isNarrowMode && shouldExpand ) {
                newMode = false;
            } else if ( !isNarrowMode && shouldCollapse ) {
                newMode = true;
            } else {
                newMode = isNarrowMode;
            }

            if ( newMode !== isNarrowMode ) {
                isNarrowMode = newMode;
                narrowTable( $table, isNarrowMode );
            }
        } ).observe( $table.parent().get( 0 ) );
    } );
}() );