wikimedia/mediawiki-extensions-UniversalLanguageSelector

View on GitHub
resources/js/ext.uls.inputsettings.js

Summary

Maintainability
D
2 days
Test Coverage
/*!
 * ULS-based ime settings panel
 *
 * Copyright (C) 2012 Alolita Sharma, Amir Aharoni, Arun Ganesh, Brandon Harris,
 * Niklas Laxström, Pau Giner, Santhosh Thottingal, Siebrand Mazeland and other
 * contributors. See CREDITS for a list.
 *
 * UniversalLanguageSelector is dual licensed GPLv2 or later and MIT. You don't
 * have to do anything special to choose one license or the other and you don't
 * have to notify anyone which license you are using. You are free to use
 * UniversalLanguageSelector in commercial projects as long as the copyright
 * header is left intact. See files GPL-LICENSE and MIT-LICENSE for details.
 *
 * @file
 * @ingroup Extensions
 * @licence GNU General Public Licence 2.0 or later
 * @licence MIT License
 */

( function () {
    'use strict';

    var template = '<div class="uls-input-settings">' +
        // Top "Display settings" title
        '<div class="row">' +
        '<div class="twelve columns">' +
        '<h3 data-i18n="ext-uls-input-settings-title"></h3>' +
        '</div>' +
        '</div>' +

        // "Language for ime", title above the buttons row
        '<div class="row enabled-only uls-input-settings-languages-title">' +
        '<div class="twelve columns">' +
        '<h4 data-i18n="ext-uls-input-settings-ui-language"></h4>' +
        '</div>' +
        '</div>' +

        // UI languages buttons row
        '<div class="row enabled-only">' +
        '<div class="uls-ui-languages twelve columns"></div>' +
        '</div>' +

        // Web IMEs enabling chechbox with label
        '<div class="row enabled-only">' +
        '<div class="twelve columns">' +
        '<div class="uls-input-settings-inputmethods-list">' +
        // "Input settings for language xyz" title
        '<h4 class="uls-input-settings-imes-title"></h4>' +
        '</div>' +
        '</div>' +
        '</div>' +

        // Disable IME system button
        '<div class="row">' +
        '<div class="twelve columns uls-input-settings-disable-info"></div>' +
        '<div class="ten columns uls-input-settings-toggle">' +
        '<button class="cdx-button cdx-button--type-primary cdx-button--action-progressive active uls-input-toggle-button"></button>' +
        '</div>' +
        '</div>';

    function InputSettings( $parent ) {
        this.nameI18n = 'ext-uls-input-settings-title-short';
        this.descriptionI18n = 'ext-uls-input-settings-desc';
        this.$template = $( template );
        this.uiLanguage = this.getInterfaceLanguage();
        this.contentLanguage = this.getContentLanguage();
        this.$parent = $parent;
        // ime system is lazy loaded, make sure it is initialized
        mw.ime.init();
        this.savedRegistry = $.extend( true, {}, $.ime.preferences.registry );
    }

    InputSettings.prototype = {

        constructor: InputSettings,

        /**
         * Render the module into a given target
         */
        render: function () {
            var $enabledOnly,
                webfonts = $( document.body ).data( 'webfonts' );

            this.dirty = false;
            this.$parent.$settingsPanel.empty();
            this.$parent.$settingsPanel.append( this.$template );
            $enabledOnly = this.$template.find( '.enabled-only' );
            if ( $.ime.preferences.isEnabled() ) {
                $enabledOnly.removeClass( 'hide' );
            } else {
                // Hide the language list and ime selector
                $enabledOnly.addClass( 'hide' );
            }

            this.prepareLanguages();
            this.prepareToggleButton();
            this.$parent.i18n();

            if ( webfonts ) {
                webfonts.refresh();
            }

            this.listen();
        },

        /**
         * Mark dirty, there are unsaved changes. Enable the apply button.
         * Useful in many places when something changes.
         */
        markDirty: function () {
            this.dirty = true;
            this.$parent.enableApplyButton();
        },

        prepareInputmethods: function ( language ) {
            var index, inputSettings, $imeListContainer, defaultInputmethod,
                imes, selected, imeId, $imeListTitle;

            imes = $.ime.languages[ language ];

            $imeListTitle = this.$template.find( '.uls-input-settings-imes-title' );
            $imeListContainer = this.$template.find( '.uls-input-settings-inputmethods-list' );

            $imeListContainer.empty();

            if ( !imes ) {
                $imeListContainer.append( $( '<label>' )
                    .addClass( 'uls-input-settings-no-inputmethods' )
                    .text( $.i18n( 'ext-uls-input-settings-noime' ) ) );
                $imeListTitle.text( '' );
                return;
            }

            $imeListTitle.text( $.i18n( 'ext-uls-input-settings-ime-settings',
                $.uls.data.getAutonym( language ) ) );

            inputSettings = this;

            defaultInputmethod = $.ime.preferences.getIM( language ) || imes.inputmethods[ 0 ];

            for ( index in imes.inputmethods ) {
                imeId = imes.inputmethods[ index ];
                selected = defaultInputmethod === imeId;
                $imeListContainer.append( inputSettings.renderInputmethodOption( imeId,
                    selected ) );
            }

            $imeListContainer.append( inputSettings.renderInputmethodOption( 'system',
                defaultInputmethod === 'system' ) );

            // Added input methods may increase the height of window. Make sure
            // the entire window is in view port
            this.$parent.position();
        },

        /**
         * For the given input method id, render the selection option.
         *
         * @param {string} imeId Input method id
         * @param {boolean} selected Whether the input is the currently selected one.
         * @return {Object} jQuery object corresponding to the input method item.
         */
        renderInputmethodOption: function ( imeId, selected ) {
            var $imeLabel, name, description, $helplink, inputmethod, $inputMethodItem;

            if ( imeId !== 'system' && !$.ime.sources[ imeId ] ) {
                // imeId not known for jquery.ime.
                // It is very rare, but still validate it.
                return $();
            }

            $imeLabel = $( '<label>' ).attr( 'for', imeId ).addClass( 'cdx-radio__label' );

            $inputMethodItem = $( '<input>' ).attr( {
                type: 'radio',
                class: 'cdx-radio__input',
                name: 'ime',
                id: imeId,
                value: imeId
            } )
                .prop( 'checked', selected );

            if ( imeId === 'system' ) {
                name = $.i18n( 'ext-uls-disable-input-method' );
                description = '';
                $helplink = '';
            } else {
                inputmethod = $.ime.inputmethods[ imeId ];
                $helplink = $( '<a>' )
                    .addClass( 'uls-ime-help' )
                    .text( $.i18n( 'ext-uls-ime-help' ) )
                    .attr( 'href', mw.msg( 'uls-ime-helppage', imeId ) )
                    .attr( 'target', '_blank' );
                if ( !inputmethod ) {
                    // The input method definition(rules) not loaded.
                    // We will show the name from $.ime.sources
                    name = $.ime.sources[ imeId ].name;
                    description = '';
                } else {
                    name = inputmethod.name;
                    description = $.ime.inputmethods[ imeId ].description;
                }
            }

            $imeLabel.append(
                $( '<strong>' )
                    .addClass( 'uls-input-settings-name' )
                    .text( name + ' ' ),
                $( '<span>' )
                    .addClass( 'uls-input-settings-description' )
                    .text( description ),
                $helplink
            );

            var $icon = $( '<span>' ).addClass( 'cdx-radio__icon' );
            return $( '<div>' )
                .addClass( 'cdx-radio' )
                .append( $inputMethodItem, $icon, $imeLabel );
        },

        /**
         * Prepare the UI language selector
         */
        prepareLanguages: function () {
            var inputSettings = this,
                SUGGESTED_LANGUAGES_NUMBER = 3,
                selectedImeLanguage = $.ime.preferences.getLanguage(),
                languagesForButtons, $languages, suggestedLanguages,
                lang, i, language, $button, $caret;

            $languages = this.$template.find( '.uls-ui-languages' );

            suggestedLanguages = this.frequentLanguageList()
                // Common world languages, for the case that there are
                // too few suggested languages
                .concat( [ 'en', 'zh', 'fr' ] );

            // Content language is always on the first button

            languagesForButtons = [ this.contentLanguage ];

            // This is needed when drawing the panel for the second time
            // after selecting a different language
            $languages.empty();

            // Selected IME language may be different, and it must be present, too
            if ( $.uls.data.languages[ selectedImeLanguage ] &&
                languagesForButtons.indexOf( selectedImeLanguage ) === -1
            ) {
                languagesForButtons.push( selectedImeLanguage );
            }

            // UI language must always be present
            if ( this.uiLanguage !== this.contentLanguage &&
                $.uls.data.languages[ this.uiLanguage ] &&
                languagesForButtons.indexOf( this.uiLanguage ) === -1 ) {
                languagesForButtons.push( this.uiLanguage );
            }

            for ( lang in suggestedLanguages ) {
                // Skip already found languages
                if ( languagesForButtons.indexOf( suggestedLanguages[ lang ] ) > -1 ) {
                    continue;
                }

                languagesForButtons.push( suggestedLanguages[ lang ] );

                // No need to add more languages than buttons
                if ( languagesForButtons.length >= SUGGESTED_LANGUAGES_NUMBER ) {
                    break;
                }
            }

            function buttonHandler( button ) {
                return function () {
                    var selectedLang = button.data( 'language' );

                    if ( selectedLang !== $.ime.preferences.getLanguage() ) {
                        inputSettings.markDirty();
                        $.ime.preferences.setLanguage( selectedLang );
                    }
                    // Mark the button selected
                    $( '.uls-ui-languages .cdx-button' ).removeClass( 'uls-cdx-button-pressed' );
                    button.addClass( 'uls-cdx-button-pressed' );
                    inputSettings.prepareInputmethods( selectedLang );
                };
            }

            // In case no preference exist for IME, selected language is contentLanguage
            selectedImeLanguage = selectedImeLanguage || this.contentLanguage;
            // Add the buttons for the most likely languages
            for ( i = 0; i < SUGGESTED_LANGUAGES_NUMBER; i++ ) {
                language = languagesForButtons[ i ];
                $button = $( '<button>' )
                    .addClass( 'cdx-button uls-language-button autonym' )
                    .text( $.uls.data.getAutonym( language ) )
                    .prop( {
                        lang: language,
                        dir: $.uls.data.getDir( language )
                    } );

                $button.data( 'language', language );
                $caret = $( '<span>' ).addClass( 'uls-input-settings-caret' );

                $languages.append( $button, $caret );

                $button.on( 'click', buttonHandler( $button ) );

                if ( language === selectedImeLanguage ) {
                    $button.trigger( 'click' );
                }
            }

            this.prepareMoreLanguages();
        },

        /**
         * Prepare the more languages button. It is a ULS trigger
         */
        prepareMoreLanguages: function () {
            var inputSettings = this,
                $languages, $moreLanguagesButton;

            $languages = this.$template.find( '.uls-ui-languages' );
            $moreLanguagesButton = $( '<button>' )
                .prop( 'class', 'uls-more-languages' )
                .addClass( 'cdx-button' ).text( '...' );

            $languages.append( $moreLanguagesButton );
            // Show the long language list to select a language for ime settings
            $moreLanguagesButton.uls( {
                left: inputSettings.$parent.left,
                top: inputSettings.$parent.top,
                onReady: function () {
                    var uls = this,
                        $wrap,
                        $back = $( '<div>' )
                            .addClass( 'uls-icon-back' )
                            .data( 'i18n', 'ext-uls-back-to-input-settings' )
                            .i18n()
                            .text( ' ' );

                    $back.on( 'click', function () {
                        uls.hide();
                        inputSettings.$parent.show();
                    } );

                    $wrap = $( '<div>' )
                        .addClass( 'uls-search-wrapper-wrapper' );

                    uls.$menu.find( '.uls-search-wrapper' ).wrap( $wrap );
                    uls.$menu.find( '.uls-search-wrapper-wrapper' ).prepend( $back );

                    // Copy callout related classes from parent
                    // eslint-disable-next-line no-jquery/no-class-state
                    uls.$menu.toggleClass( 'selector-left', inputSettings.$parent.$window.hasClass( 'selector-left' ) );
                    // eslint-disable-next-line no-jquery/no-class-state
                    uls.$menu.toggleClass( 'selector-right', inputSettings.$parent.$window.hasClass( 'selector-right' ) );
                },
                onVisible: function () {
                    var $parent;

                    this.$menu.find( '.uls-languagefilter' )
                        .prop( 'placeholder', $.i18n( 'ext-uls-input-settings-ui-language' ) );

                    // eslint-disable-next-line no-jquery/no-class-state
                    if ( !inputSettings.$parent.$window.hasClass( 'callout' ) ) {
                        // callout menus will have position rules. others use
                        // default position
                        return;
                    }

                    $parent = $( '#language-settings-dialog' );

                    // Re-position the element according to the window that called it
                    if ( parseInt( $parent.css( 'left' ), 10 ) ) {
                        this.$menu.css( 'left', $parent.css( 'left' ) );
                    }
                    if ( parseInt( $parent.css( 'top' ), 10 ) ) {
                        this.$menu.css( 'top', $parent.css( 'top' ) );
                    }

                    // eslint-disable-next-line no-jquery/no-class-state
                    if ( inputSettings.$parent.$window.hasClass( 'callout' ) ) {
                        this.$menu.addClass( 'callout callout--languageselection' );
                    } else {
                        this.$menu.removeClass( 'callout' );
                    }
                },
                onSelect: function ( langCode ) {
                    $.ime.preferences.setLanguage( langCode );
                    inputSettings.$parent.show();
                    inputSettings.prepareLanguages();
                    inputSettings.markDirty();
                },
                languages: mw.ime.getLanguagesWithIME(),
                ulsPurpose: 'input-settings'
            } );

            $moreLanguagesButton.on( 'click', function () {
                inputSettings.$parent.hide();
                mw.hook( 'mw.uls.ime.morelanguages' ).fire();
            } );
        },

        prepareToggleButton: function () {
            var $toggleButton, $toggleButtonDesc;

            $toggleButton = this.$template.find( '.uls-input-toggle-button' );
            $toggleButtonDesc = this.$template
                .find( '.uls-input-settings-disable-info' );

            if ( $.ime.preferences.isEnabled() ) {
                $toggleButton.data( 'i18n', 'ext-uls-input-disable' );
                $toggleButtonDesc.hide();
            } else {
                $toggleButton.data( 'i18n', 'ext-uls-input-enable' );
                $toggleButtonDesc.data( 'i18n', 'ext-uls-input-disable-info' ).show();
            }

            $toggleButton.i18n();
            $toggleButtonDesc.i18n();
        },

        /**
         * Get previous languages
         *
         * @return {Array}
         */
        frequentLanguageList: function () {
            return mw.uls.getFrequentLanguageList();
        },

        /**
         * Get the current user interface language.
         *
         * @return {string} Current UI language
         */
        getInterfaceLanguage: function () {
            return mw.config.get( 'wgUserLanguage' );
        },

        /**
         * Get the current content language.
         *
         * @return {string} Current content language
         */
        getContentLanguage: function () {
            return mw.config.get( 'wgContentLanguage' );
        },

        /**
         * Register general event listeners
         */
        listen: function () {
            var inputSettings = this,
                $imeListContainer;

            $imeListContainer = this.$template.find( '.uls-input-settings-inputmethods-list' );

            $imeListContainer.on( 'change', 'input:radio[name=ime]:checked', function () {
                inputSettings.markDirty();
                $.ime.preferences.setIM( $( this ).val() );
            } );

            inputSettings.$template.find( 'button.uls-input-toggle-button' )
                .on( 'click', function () {
                    inputSettings.markDirty();

                    if ( $.ime.preferences.isEnabled() ) {
                        inputSettings.disableInputTools();
                    } else {
                        inputSettings.enableInputTools();
                    }
                } );

        },

        /**
         * Disable input tools
         */
        disableInputTools: function () {
            $.ime.preferences.disable();
            mw.ime.disable();
            this.$template.find( '.enabled-only' ).addClass( 'hide' );
            this.prepareToggleButton();
        },

        /**
         * Enable input tools
         */
        enableInputTools: function () {
            $.ime.preferences.enable();
            mw.ime.setup();
            this.$template.find( '.enabled-only' ).removeClass( 'hide' );
            this.$template.scrollIntoView();
            this.prepareToggleButton();
        },

        /**
         * Close the language settings window.
         * Depending on the context, actions vary.
         */
        close: function () {
            this.$parent.close();
        },

        /**
         * Callback for save preferences
         *
         * @param {boolean} success
         */
        onSave: function ( success ) {
            if ( success ) {
                // Live ime update
                this.$parent.hide();
                // Disable apply button
                this.$parent.disableApplyButton();
            }
            // FIXME in case of failure. what to do?!
        },

        /**
         * Handle the apply button press.
         * Note that the button press may not be from the input settings module.
         * For example, a user can change input settings and then go to display settings panel,
         * do some changes and press apply button there. That press is applicable for all
         * modules.
         */
        apply: function () {
            var previousIM,
                inputSettings = this,
                previousLanguage = inputSettings.savedRegistry.language,
                currentlyEnabled = $.ime.preferences.isEnabled(),
                currentLanguage = $.ime.preferences.getLanguage(),
                currentIM = $.ime.preferences.getIM( currentLanguage );

            if ( !inputSettings.dirty ) {
                // No changes to save in this module.
                return;
            }
            inputSettings.$parent.setBusy( true );

            if ( previousLanguage ) {
                previousIM = inputSettings.savedRegistry.imes[ previousLanguage ];
            }

            if ( currentLanguage !== inputSettings.savedRegistry.language ||
                currentIM !== previousIM
            ) {
                mw.hook( 'mw.uls.ime.change' ).fire( currentIM );
            }

            if ( inputSettings.savedRegistry.enable !== currentlyEnabled ) {
                mw.hook( currentlyEnabled ? 'mw.uls.ime.enable' : 'mw.uls.ime.disable' )
                    .fire( 'inputsettings' );
            }

            // Save the preferences
            $.ime.preferences.save( function ( result ) {
                // closure for not losing the scope
                inputSettings.onSave( result );
                inputSettings.dirty = false;
                // Update the back-up preferences for the case of canceling
                inputSettings.savedRegistry = $.extend( true, {}, $.ime.preferences.registry );
                inputSettings.$parent.setBusy( false );
            } );
        },

        /**
         * Cancel the changes done by user for input settings
         */
        cancel: function () {
            if ( !this.dirty ) {
                this.close();
                return;
            }
            // Reload preferences
            $.ime.preferences.registry = $.extend( true, {}, this.savedRegistry );
            this.uiLanguage = this.getInterfaceLanguage();
            this.contentLanguage = this.getContentLanguage();
            // Restore the state of IME
            if ( $.ime.preferences.isEnabled() ) {
                mw.ime.setup();
            } else {
                mw.ime.disable();
            }
            this.close();
        }
    };

    // Register this module to language settings modules
    $.fn.languagesettings.modules = Object.assign( $.fn.languagesettings.modules, {
        input: InputSettings
    } );

}() );