const View = require( '../mobile.startup/View' ), util = require( '../mobile.startup/util' ), langUtil = require( './util' ), mfExtend = require( '../mobile.startup/mfExtend' ); /** * @class Hooks~LanguageSearcher * @classdesc Overlay displaying a structured list of languages for a page, only accessible via * the {@link Hooks~'mobileFrontend.languageSearcher.onOpen' mobileFrontend.languageSearcher.onOpen hook}. * @hideconstructor * @extends module:mobile.startup/View * @param {Object} props Configuration options * @param {Object[]} props.languages list of language objects as returned by the API * @param {Array|boolean} props.variants language variant objects * or false if no variants exist * @param {boolean} props.showSuggestedLanguages If the suggested languages * section should be rendered. * @param {string} [props.deviceLanguage] the device's primary language * @param {Function} [props.onOpen] callback that fires on opening the searcher */ function LanguageSearcher( props ) { /** * @prop {StructuredLanguages} languages` JSDoc. */ const languages = langUtil.getStructuredLanguages( props.languages, props.variants, langUtil.getFrequentlyUsedLanguages(), props.showSuggestedLanguages, props.deviceLanguage ); View.call( this, util.extend( { className: 'language-searcher', events: { 'click a': 'onLinkClick', 'click .language-search-banner': 'onSearchBannerClick', 'input .search': 'onSearchInput' }, // the rest are template properties inputPlaceholder: mw.msg( 'mobile-frontend-languages-structured-overlay-search-input-placeholder' ), // we can't rely on CSS only to uppercase the headings. See https://stackoverflow.com/questions/3777443/css-text-transform-not-working-properly-for-turkish-characters allLanguagesHeader: mw.msg( 'mobile-frontend-languages-structured-overlay-all-languages-header' ).toLocaleUpperCase(), suggestedLanguagesHeader: mw.msg( 'mobile-frontend-languages-structured-overlay-suggested-languages-header' ).toLocaleUpperCase(), noResultsFoundHeader: mw.msg( 'mobile-frontend-languages-structured-overlay-no-results' ), noResultsFoundMessage: mw.msg( 'mobile-frontend-languages-structured-overlay-no-results-body' ), allLanguages: languages.all, allLanguagesCount: languages.all.length, suggestedLanguages: languages.suggested, suggestedLanguagesCount: languages.suggested.length, showSuggestedLanguagesHeader: languages.suggested.length > 0 }, props ) ); // defer event to be emitted after event handler has been registered const onOpen = props.onOpen; if ( !onOpen ) { return; } const searcher = this; setTimeout( () => { onOpen( searcher ); }, 0 ); } mfExtend( LanguageSearcher, View, { /** * @inheritdoc * @memberof LanguageSearcher * @instance */ template: util.template( `
{{#showSuggestedLanguagesHeader}}

{{suggestedLanguagesHeader}}

{{/showSuggestedLanguagesHeader}} {{#suggestedLanguagesCount}} {{/suggestedLanguagesCount}} {{#bannerHTML}}
{{{.}}}
{{/bannerHTML}} {{#allLanguagesCount}}

{{allLanguagesHeader}} ({{allLanguagesCount}})

{{/allLanguagesCount}}
` ), /** * @inheritdoc * @memberof LanguageSearcher * @instance */ postRender: function () { // cache this.$siteLinksList = this.$el.find( '.site-link-list' ); this.$languageItems = this.$siteLinksList.find( 'a' ); this.$subheaders = this.$el.find( 'h3' ); this.$emptyResultsSection = this.$el.find( '.empty-results' ); }, /** * Method that can be called outside MF extension to render * a banner inside the language overlay. * * Stable for use inside ContentTranslation * @memberof LanguageSearcher * @param {string} bannerHTML * @param {string} firstMissingLanguage */ addBanner: function ( bannerHTML, firstMissingLanguage ) { this.options.bannerHTML = bannerHTML; this.options.bannerFirstLanguage = firstMissingLanguage; this.options.showSuggestedLanguagesHeader = true; this.render(); }, onSearchBannerClick: function () { this.$el.find( '.search' ).val( this.options.bannerFirstLanguage ).trigger( 'input' ); }, /** * Article link click event handler * * @memberof LanguageSearcher * @instance * @param {jQuery.Event} ev */ onLinkClick: function ( ev ) { const $link = this.$el.find( ev.currentTarget ), lang = $link.attr( 'lang' ); /** * Internal for use in GrowthExperiments only. * * @event ~'mobileFrontend.languageSearcher.linkClick' * @memberof Hooks */ mw.hook( 'mobileFrontend.languageSearcher.linkClick' ).fire( lang ); langUtil.saveLanguageUsageCount( lang, langUtil.getFrequentlyUsedLanguages() ); }, /** * Search input handler * * @memberof LanguageSearcher * @instance * @param {jQuery.Event} ev Event object. */ onSearchInput: function ( ev ) { const searchOrigin = ev.originalEvent === undefined ? 'entrypoint-banner' : 'ui'; this.filterLanguages( ev.target.value.toLowerCase(), searchOrigin ); }, /** * Filter the language list to only show languages that match the current search term. * * @memberof LanguageSearcher * @instance * @param {string} searchQuery of search term (lowercase). * @param {'entrypoint-banner'|'ui'} searchOrigin for internal use by CX entrypoints only */ filterLanguages: function ( searchQuery, searchOrigin ) { const filteredList = []; if ( searchQuery ) { this.options.languages.forEach( function ( language ) { const langname = language.langname; // search by language code or language name if ( language.autonym.toLowerCase().indexOf( searchQuery ) > -1 || ( langname && langname.toLowerCase().indexOf( searchQuery ) > -1 ) || language.lang.toLowerCase().indexOf( searchQuery ) > -1 ) { filteredList.push( language.lang ); } } ); if ( this.options.variants ) { this.options.variants.forEach( function ( variant ) { // search by variant code or variant name if ( variant.autonym.toLowerCase().indexOf( searchQuery ) > -1 || variant.lang.toLowerCase().indexOf( searchQuery ) > -1 ) { filteredList.push( variant.lang ); } } ); } this.$languageItems.addClass( 'hidden' ); if ( filteredList.length ) { this.$siteLinksList.find( `.${ mw.util.escapeRegExp( filteredList.join( ',.' ) ) }` ).removeClass( 'hidden' ); this.$emptyResultsSection.addClass( 'hidden' ); } else { this.$emptyResultsSection.removeClass( 'hidden' ); // Fire with the search query and the DOM element corresponding to no-results // message so that it can be customized in hook handler /** * Internal for use in ContentTranslation only. * * @event ~'mobileFrontend.editorOpening' * @memberof Hooks */ mw.hook( 'mobileFrontend.languageSearcher.noresults' ) .fire( searchQuery, this.$emptyResultsSection.get( 0 ), searchOrigin ); } this.$siteLinksList.addClass( 'filtered' ); this.$subheaders.addClass( 'hidden' ); } else { this.$languageItems.removeClass( 'hidden' ); this.$siteLinksList.removeClass( 'filtered' ); this.$subheaders.removeClass( 'hidden' ); this.$emptyResultsSection.addClass( 'hidden' ); } } } ); module.exports = LanguageSearcher;