wikimedia/mediawiki-extensions-UniversalLanguageSelector

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

Summary

Maintainability
F
3 days
Test Coverage
/*!
 * ULS-based display 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-display-settings">' +

        // Tab switcher buttons
        '<div class="row">' +
        '<div class="twelve columns uls-display-settings-tab-switcher">' +
        '<div class="uls-button-group cdx-button-group">' +
        '<button id="uls-display-settings-language-tab" class="cdx-button uls-cdx-button-pressed" ' +
        'data-i18n="ext-uls-display-settings-language-tab"></button>' +
        '<button id="uls-display-settings-fonts-tab" class="cdx-button" data-i18n="ext-uls-display-settings-fonts-tab"></button>' +
        '</div>' +
        '</div>' +
        '</div>' +

        // Begin display language sub-panel
        '<div class="uls-sub-panel uls-display-settings-language-tab">' +

        // "Display language", title above the buttons row
        '<div class="row">' +
        '<div class="twelve columns">' +
        '<h4 data-i18n="ext-uls-display-settings-ui-language"></h4>' +
        '</div>' +
        '</div>' +

        // UI languages buttons row
        '<div class="row">' +
        '<div class="uls-ui-languages twelve columns">' +
        '<p data-i18n="ext-uls-language-buttons-help"></p>' +
        '</div>' +
        '</div>' +

        // End display language section
        '</div>' +

        // Begin font settings section, hidden by default
        '<div class="uls-sub-panel uls-display-settings-fonts-tab hide">' +

        // "Font settings" title
        '<div class="row">' +
        '<div class="twelve columns">' +
        '<h4 data-i18n="ext-uls-display-settings-font-settings"></h4>' +
        '</div>' +
        '</div>' +

        '<div id="uls-display-settings-font-selectors" class="uls-display-settings-font-selectors">' +

        // Menus font selection dropdown with label
        '<div class="row uls-font-item uls-content-fonts">' +
        '<div class="six columns">' +
        '<label class="uls-font-label" id="content-font-selector-label"></label>' +
        '</div>' +
        '<select id="content-font-selector" class="four columns end uls-font-select"></select>' +
        '</div>' +

        // Content font selection dropdown with label
        '<div class="row uls-font-item uls-ui-fonts">' +
        '<div class="six columns">' +
        '<label class="uls-font-label" id="ui-font-selector-label"></label>' +
        '</div>' +
        '<select id="ui-font-selector" class="four columns end uls-font-select"></select>' +
        '</div>' +

        // End font selectors
        '</div>' +

        // Webfonts enabling checkbox with label
        '<div class="row">' +
        '<div class="twelve columns">' +
        '<div class="cdx-checkbox">' +
        '<input type="checkbox" id="webfonts-enable-checkbox" class="cdx-checkbox__input" />' +
        '<span class="cdx-checkbox__icon"></span>' +
        '<label class="checkbox cdx-checkbox__label" for="webfonts-enable-checkbox" >' +
        '<strong data-i18n="ext-uls-webfonts-settings-title"></strong> ' +
        '<span data-i18n="ext-uls-webfonts-settings-info"></span> ' +
        '<a target="_blank" href="https://www.mediawiki.org/wiki/Universal_Language_Selector/WebFonts" data-i18n="ext-uls-webfonts-settings-info-link"></a>' +
        '</label>' +
        '</div>' +
        '</div>' +
        '</div>' +

        // End font settings section
        '</div>';

    function DisplaySettings( $parent ) {
        this.nameI18n = 'ext-uls-display-settings-title-short';
        this.descriptionI18n = 'ext-uls-display-settings-desc';
        this.$template = $( template );
        this.uiLanguage = this.getUILanguage();
        this.contentLanguage = this.getContentLanguage();
        this.$webfonts = null;
        this.$parent = $parent;
        this.savedRegistry = $.extend( true, {}, mw.webfonts.preferences );
        this.dirty = false;
    }

    DisplaySettings.prototype = {

        constructor: DisplaySettings,

        /**
         * Loads the webfonts module sets the `webfonts` property when its safe to do so
         *
         * @return {jQuery.Promise}
         */
        setupWebFonts: function () {
            var d = $.Deferred();
            mw.loader.using( [ 'ext.uls.webfonts.repository', '@wikimedia/codex' ] ).then( function () {
                if ( this.isWebFontsEnabled ) {
                    mw.webfonts.setup();
                }

                // Allow the webfonts library to finish loading (hack)
                setTimeout( function () {
                    this.$webfonts = $( document.body ).data( 'webfonts' );
                    d.resolve();
                }.bind( this ), 1 );
            }.bind( this ) );
            return d.promise();
        },
        /**
         * Render the module into a given target
         */
        render: function () {
            this.setupWebFonts().then( function () {
                this.renderAfterDependenciesLoaded();
            }.bind( this ) );
        },
        /**
         * Render the module into a given target after all
         */
        renderAfterDependenciesLoaded: function () {
            this.$parent.$settingsPanel.empty();
            this.$parent.$settingsPanel.append( this.$template );
            this.prepareLanguages();
            this.prepareUIFonts();
            this.prepareContentFonts();
            this.prepareWebfontsCheckbox();

            // Usually this is already loaded, but when changing language it
            // might not be.
            this.preview( this.uiLanguage );
            this.listen();
        },

        prepareWebfontsCheckbox: function () {
            var webFontsEnabled = this.isWebFontsEnabled();

            if ( !webFontsEnabled ) {
                $( '#uls-display-settings-font-selectors' ).addClass( 'hide' );
            }

            $( '#webfonts-enable-checkbox' ).prop( 'checked', webFontsEnabled );
        },

        isWebFontsEnabled: function () {
            return mw.webfonts.preferences.isEnabled();
        },

        /**
         * Prepare the UI language selector
         */
        prepareLanguages: function () {
            var $loginCta,
                displaySettings = this,
                SUGGESTED_LANGUAGES_NUMBER = 3,
                anonsAllowed = mw.config.get( 'wgULSAnonCanChangeLanguage' ),
                languagesForButtons, $languages, suggestedLanguages,
                lang, i, language, $button, autonym;

            // Don't let anonymous users change interface language
            if ( !anonsAllowed && !mw.user.isNamed() ) {
                $loginCta = $( '<p>' )
                    .attr( 'id', 'uls-display-settings-anon-log-in-cta' );
                autonym = $.uls.data.getAutonym( this.contentLanguage );

                this.$template.find( '.uls-display-settings-language-tab' )
                    .empty()
                    .append(
                        $( '<p>' ).append(
                            $( '<span>' )
                                .addClass( 'uls-display-settings-anon-label' )
                                .text( $.i18n( 'ext-uls-display-settings-anon-label' ) + '\u00A0' ),
                            $( '<span>' )
                                .text( $.i18n( 'ext-uls-display-settings-anon-same-as-content', autonym ) )
                        ),
                        $loginCta
                    );

                new mw.Api().parse( $.i18n( 'ext-uls-display-settings-anon-log-in-cta' ) )
                    .done( function ( parsedCta ) {
                        // The parsed CTA is HTML
                        $loginCta.html( parsedCta );
                        $loginCta.find( 'a' ).on( 'click', function () {
                            // If EventLogging is installed and enabled for ULS, give it a
                            // chance to log this event. There is no promise provided and in
                            // most browsers this will use the Beacon API in the background.
                            // In older browsers, this event will likely get lost.
                            mw.hook( 'mw.uls.login.click' );
                        } );
                    } );

                return;
            }

            $languages = this.$template.find( 'div.uls-ui-languages' );
            suggestedLanguages = this.frequentLanguageList()
                // Common world languages, for the case that there are
                // too few suggested languages
                .concat( [ 'en', 'zh-hans', 'zh-hant', '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.find( 'button' ).remove();

            // UI language must always be present
            if ( this.uiLanguage !== this.contentLanguage ) {
                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 () {
                    displaySettings.markDirty();
                    displaySettings.uiLanguage = button.data( 'language' ) || displaySettings.uiLanguage;
                    $( 'div.uls-ui-languages button.cdx-button' ).removeClass( 'uls-cdx-button-pressed' );
                    button.addClass( 'uls-cdx-button-pressed' );
                    displaySettings.prepareUIFonts();
                    displaySettings.preview( displaySettings.uiLanguage );
                };
            }

            // 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 )
                    } );

                if ( language === this.uiLanguage ) {
                    $button.addClass( 'uls-cdx-button-pressed' );
                }

                $button.data( 'language', language );
                $languages.append( $button );
                $button.on( 'click', buttonHandler( $button ) );
            }

            this.prepareMoreLanguages();
        },

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

            $languages = this.$template.find( 'div.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 display settings
            $moreLanguagesButton.uls( {
                onPosition: this.$parent.position.bind( this.$parent ),
                onReady: function () {
                    var $wrap,
                        uls = this,
                        $back = $( '<div>' )
                            .addClass( 'uls-icon-back' );

                    $back.on( 'click', function () {
                        uls.hide();
                        displaySettings.$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', displaySettings.$parent.$window.hasClass( 'selector-left' ) );
                    // eslint-disable-next-line no-jquery/no-class-state
                    uls.$menu.toggleClass( 'selector-right', displaySettings.$parent.$window.hasClass( 'selector-right' ) );
                },
                onVisible: function () {
                    this.$menu.find( '.uls-languagefilter' )
                        .prop( 'placeholder', $.i18n( 'ext-uls-display-settings-ui-language' ) );

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

                    // If the ULS is shown in the sidebar,
                    // add a caret pointing to the icon
                    // eslint-disable-next-line no-jquery/no-class-state
                    if ( displaySettings.$parent.$window.hasClass( 'callout' ) ) {
                        this.$menu.addClass( 'callout callout--languageselection' );
                    } else {
                        this.$menu.removeClass( 'callout' );
                    }
                },
                onSelect: function ( langCode ) {
                    displaySettings.uiLanguage = langCode;
                    displaySettings.$template.attr( 'lang', langCode );
                    // This re-renders the whole thing
                    displaySettings.$parent.show();
                    // And the only thing we need to take care of is to enable
                    // the apply button
                    displaySettings.markDirty();
                },
                ulsPurpose: 'interface-language',
                quickList: function () {
                    return mw.uls.getFrequentLanguageList();
                }
            } );

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

        /**
         * Preview the settings panel in the given language
         *
         * @param {string} language Language code
         */
        preview: function ( language ) {
            var displaySettings = this;

            // Reset the language and font for the panel.
            this.$template.attr( 'lang', language )
                .css( 'font-family', '' );
            $.i18n().locale = language;
            mw.uls.loadLocalization( language ).done( function () {
                displaySettings.i18n();
                if ( displaySettings.$webfonts ) {
                    displaySettings.$webfonts.refresh();
                }
            } );
        },

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

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

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

        /**
         * Prepare a font selector section with a label and a selector element.
         *
         * @param {string} target 'ui' or 'content'
         */
        prepareFontSelector: function ( target ) {
            var language, fonts, $fontSelector, savedFont,
                $systemFont, $fontLabel, $fontsSection;

            // Get the language code from the right property -
            // uiLanguage or contentLanguage
            language = this[ target + 'Language' ];
            if ( this.isWebFontsEnabled() ) {
                fonts = this.$webfonts.list( language );
            } else {
                fonts = [];
            }

            // Possible classes:
            // uls-ui-fonts
            // uls-content-fonts
            $fontsSection = this.$template.find( 'div.uls-' + target + '-fonts' );

            // The section may be visible from the previous time
            // the user opened the dialog, so we need to hide it.
            if ( fonts.length === 0 ) {
                $fontsSection.hide();
                return;
            }

            $fontsSection.show();
            // Possible ids:
            // uls-ui-font-selector
            // uls-content-font-selector
            $fontSelector = this.$template.find( '#' + target + '-font-selector' );

            // Remove all current fonts
            $fontSelector.find( 'option' ).remove();

            // Get the saved font using the fontSelector defined in mw.webfonts.setup
            savedFont = this.$webfonts.getFont( language );
            fonts.forEach( function ( font ) {
                var $fontOption;

                if ( font !== 'system' ) {
                    $fontOption = $( '<option>' ).attr( 'value', font ).text( font );
                    $fontSelector.append( $fontOption );
                    $fontOption.prop( 'selected', savedFont === font );
                }
            } );

            $fontSelector.prop( 'disabled', !this.isWebFontsEnabled() );

            // Using attr() instead of data() because jquery.i18n doesn't
            // currently see latter.
            $systemFont = $( '<option>' )
                .val( 'system' )
                .attr( 'data-i18n', 'ext-uls-webfonts-system-font' );
            $fontSelector.append( $systemFont );
            $systemFont.prop( 'selected', savedFont === 'system' || !savedFont );

            // Possible ids:
            // uls-ui-font-selector-label
            // uls-content-font-selector-label
            $fontLabel = this.$template.find( '#' + target + '-font-selector-label' );
            $fontLabel.empty().append( $( '<strong>' ) );

            // Possible messages:
            // ext-uls-webfonts-select-for-ui-info
            // ext-uls-webfonts-select-for-content-info
            $fontLabel.append( $( '<div>' )
                .attr( 'data-i18n', 'ext-uls-webfonts-select-for-' + target + '-info' ) );
        },

        /**
         * i18n this settings panel
         */
        i18n: function () {
            this.$parent.i18n();
            this.$template.find( '#ui-font-selector-label strong' )
                .text( $.i18n( 'ext-uls-webfonts-select-for', $.uls.data.getAutonym( this.uiLanguage ) ) );
            this.$template.find( '#content-font-selector-label strong' )
                .text( $.i18n( 'ext-uls-webfonts-select-for', $.uls.data.getAutonym( this.contentLanguage ) ) );
        },

        /**
         * Prepare the font selector for UI language.
         */
        prepareUIFonts: function () {
            if ( this.uiLanguage === this.contentLanguage ) {
                this.$template.find( 'div.uls-ui-fonts' ).hide();
                return;
            }

            this.prepareFontSelector( 'ui' );
        },

        /**
         * Prepare the font selector for UI language.
         */
        prepareContentFonts: function () {
            this.prepareFontSelector( 'content' );
        },

        /**
         * 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();
        },

        /**
         * Register general event listeners
         */
        listen: function () {
            var displaySettings = this,
                $contentFontSelector = this.$template.find( '#content-font-selector' ),
                $uiFontSelector = this.$template.find( '#ui-font-selector' ),
                $tabButtons = displaySettings.$template.find( '.uls-display-settings-tab-switcher button' );

            $( '#webfonts-enable-checkbox' ).on( 'click', function () {
                var $fontSelectors = $( '#uls-display-settings-font-selectors' );

                displaySettings.markDirty();

                if ( this.checked ) {
                    displaySettings.setupWebFonts().then( function () {
                        mw.webfonts.preferences.enable();

                        displaySettings.prepareContentFonts();
                        displaySettings.prepareUIFonts();

                        displaySettings.i18n();
                        // eslint-disable-next-line no-jquery/no-sizzle
                        displaySettings.$webfonts.apply( $uiFontSelector.find( 'option:selected' ) );
                        displaySettings.$webfonts.refresh();

                        $fontSelectors.removeClass( 'hide' );
                    } );
                } else {
                    $fontSelectors.addClass( 'hide' );
                    mw.webfonts.preferences.disable();
                    mw.webfonts.preferences.setFont( displaySettings.uiLanguage, 'system' );
                    displaySettings.$webfonts.refresh();

                    $contentFontSelector.prop( 'disabled', true );
                    $uiFontSelector.prop( 'disabled', true );
                }
            } );

            $uiFontSelector.on( 'change', function () {
                displaySettings.markDirty();
                mw.webfonts.preferences.setFont( displaySettings.uiLanguage,
                    $( this ).val()
                );
                displaySettings.$webfonts.refresh();
            } );

            $contentFontSelector.on( 'change', function () {
                displaySettings.markDirty();
                mw.webfonts.preferences.setFont( displaySettings.contentLanguage,
                    $( this ).val()
                );
                displaySettings.$webfonts.refresh();
            } );

            $tabButtons.on( 'click', function () {
                var $button = $( this );

                // eslint-disable-next-line no-jquery/no-class-state
                if ( $button.hasClass( 'uls-cdx-button-pressed' ) ) {
                    return;
                }

                displaySettings.$template.find( '.uls-sub-panel' ).each( function () {
                    var $subPanel = $( this );

                    // eslint-disable-next-line no-jquery/no-class-state
                    if ( $subPanel.hasClass( $button.attr( 'id' ) ) ) {
                        $subPanel.removeClass( 'hide' );
                    } else {
                        $subPanel.addClass( 'hide' );
                    }
                } );

                displaySettings.$parent.position();
                $tabButtons.removeClass( 'uls-cdx-button-pressed' );
                $button.addClass( 'uls-cdx-button-pressed' );
            } ).on( 'mousedown', function ( event ) {
                // Avoid taking focus, to avoid bad looking focus styles
                event.preventDefault();
            } );

        },

        /**
         * 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 ) {
                if ( this.$webfonts ) {
                    // Live font update
                    this.$webfonts.refresh();
                }

                this.$parent.hide();
                // we delay change UI language to here, because it causes a page refresh
                if ( this.uiLanguage !== this.getUILanguage() ) {
                    mw.uls.changeLanguage( this.uiLanguage );
                }
                // Disable apply button
                this.$parent.disableApplyButton();
            } // @todo What to do in case of failure?
        },

        /**
         * 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 () {
            if ( !this.dirty ) {
                // No changes to save in this module.
                return;
            }

            this.$parent.setBusy( true );
            // Save the preferences
            mw.webfonts.preferences.save( function ( result ) {
                var newWebfontsEnable, oldWebfontsEnable, webfontsEvent,
                    newRegistry = mw.webfonts.preferences.registry,
                    oldRegistry = this.savedRegistry.registry,
                    newFonts = newRegistry.fonts || {},
                    oldFonts = oldRegistry.fonts || {};

                newWebfontsEnable = newRegistry.webfontsEnabled;
                oldWebfontsEnable = oldRegistry.webfontsEnabled;
                if ( oldWebfontsEnable === undefined ) {
                    oldWebfontsEnable = mw.config.get( 'wgULSWebfontsEnabled' );
                }

                if ( newWebfontsEnable !== oldWebfontsEnable ) {
                    webfontsEvent = newWebfontsEnable ?
                        'mw.uls.webfonts.enable' :
                        'mw.uls.webfonts.disable';
                    mw.hook( webfontsEvent ).fire( 'displaysettings' );
                }

                if ( newFonts[ this.uiLanguage ] !== oldFonts[ this.uiLanguage ] ) {
                    mw.hook( 'mw.uls.font.change' ).fire(
                        'interface', this.uiLanguage, newFonts[ this.uiLanguage ]
                    );
                }

                if ( newFonts[ this.contentLanguage ] !== oldFonts[ this.contentLanguage ] ) {
                    mw.hook( 'mw.uls.font.change' ).fire(
                        'content', this.contentLanguage, newFonts[ this.contentLanguage ]
                    );
                }

                // closure for not losing the scope
                this.onSave( result );
                this.dirty = false;
                // Update the back-up preferences for the case of canceling
                this.savedRegistry = $.extend( true, {}, mw.webfonts.preferences );
                this.$parent.setBusy( false );
            }.bind( this ) );
        },

        /**
         * Cancel the changes done by user for display settings
         */
        cancel: function () {
            if ( !this.dirty ) {
                this.close();
                return;
            }
            // Reload preferences
            mw.webfonts.preferences = $.extend( true, {}, this.savedRegistry );

            // Restore fonts
            if ( this.$webfonts ) {
                this.$webfonts.refresh();
            }

            // Restore content and UI language
            this.uiLanguage = this.getUILanguage();
            this.contentLanguage = this.getContentLanguage();

            this.close();
        }
    };

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