resources/js/ext.uls.displaysettings.js
/*!
* 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
} );
}() );