The createCompactList() function runs synchronously during the module execution burst. Due to it visually changing the page, I won't defer it with rIC for the time being, although that should be considered for the future. For this commit, I'm trying to make it fit the budget of <50ms because ULS is currently usually taking 80ms-180ms on desktop (MacBook/Chrome CPU/4), and that's during batch execution with other modules as well, thus freezing the UI thread for much longer than that. constructor: * Remove needless clone of jQuery object. Use $foo instead of $( $foo ). * Remove creation of 'interlanguageList' and 'compactList' objects that are immediately removed and re-created by init(). init/getInterlanguageList: * Use the HTMLElement.lang and HTMLAnchorElement.href properties directly instead of the DOM getAttribute(). This means stores a full url instead of a relative url, which should help avoid other bugs in the future. * Remove needless jQuery() constructor and jQuery.text() call. Use Node.textContent directly instead. * Use HTMLElement#querySelectorAll instead of jQuery#find(). init/getCompactList/../filterByLangsInText: * Avoid jQuery() constructor and jQuery.attr(), use the HTMLElement.lang property directly. * Avoid jQuery() selector, call querySelectorAll directly. init/getCompactList/../getCommonLanguages/../getFrequentLanguageList: * Avoid temporary array copies from concat() and function overhead with forEach() and filter(). Instead, keep only a single array, and iterate it once. init/getCompactList/../filterByBadges (~10m -> ~0.5ms): * Use one query via $(), instead of two queries $()+find(). * Use $.map() directly instead of map()+fakejQueryObject+toArray(). * Use querySelector(One) for the child instead of $()+find(). * Use HTMLElement.lang property directly. init/hideOriginal (~5m -> ~0.8ms): * Use querySelectorAll() directly instead of jQuery find(). * Set HTMLElement.style directly instead of jQuery() css(). init/render/addTrigger: * Use createElement() and direct properties instead of $(), addClass(), prop() and text(). * The mw.msg() calls use text() and jqueryMsg#parser which is expensive. Use plain() for 'ext-uls-compact-link-info', which doesn't need parsing. Keep text() for the other message, and document why. init/listen: * Use async Deferred#then() instead of sometimes-sync Deferred#done(). Bug: T127328 Change-Id: I424c34fb82c8e95407f7b934e6d42019becbf909
561 lines
17 KiB
JavaScript
561 lines
17 KiB
JavaScript
/*!
|
|
* Compact the interlanguage links in the sidebar
|
|
*
|
|
* Copyright (C) 2012-2014 Alolita Sharma, Amir Aharoni, Arun Ganesh, Brandon Harris,
|
|
* Niklas Laxström, Pau Giner, Santhosh Thottingal, Siebrand Mazeland, Niharika Kohli
|
|
* 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 GPL-2.0-or-later
|
|
* @licence MIT License
|
|
*/
|
|
|
|
( function () {
|
|
'use strict';
|
|
|
|
var DEFAULT_LIST_SIZE = 9;
|
|
|
|
/**
|
|
* Concatenate two arrays, remove duplicates
|
|
*
|
|
* @param {Array} a First array
|
|
* @param {Array} b Second array
|
|
* @return {Array} Resulting array
|
|
*/
|
|
function concatWithoutDuplicates( a, b ) {
|
|
return a.concat( b.filter( function ( item ) {
|
|
return a.indexOf( item ) < 0;
|
|
} ) );
|
|
}
|
|
|
|
/**
|
|
* Normalize a language code for ULS usage.
|
|
*
|
|
* MediaWiki language codes (especially on WMF sites) are inconsistent
|
|
* with ULS codes. We need to use ULS codes to access the proper data.
|
|
*
|
|
* @param {string} code
|
|
* @return {string} Normalized language code
|
|
*/
|
|
function convertMediaWikiLanguageCodeToULS( code ) {
|
|
code = code.toLowerCase();
|
|
return $.uls.data.isRedirect( code ) || code;
|
|
}
|
|
|
|
/**
|
|
* Filter the language list by previous languages.
|
|
* Not all previous languages will be present in interlanguage links,
|
|
* so we are filtering them.
|
|
*
|
|
* @param {string[]} languages Language codes
|
|
* @return {string[]} List of language codes supported by the article
|
|
*/
|
|
function filterByPreviousLanguages( languages ) {
|
|
var previousLanguages = mw.uls.getPreviousLanguages();
|
|
|
|
return previousLanguages.filter( function ( language ) {
|
|
return languages.indexOf( language ) >= 0;
|
|
} );
|
|
}
|
|
|
|
/**
|
|
* Filter by languages that appear in the Babel box on the user page.
|
|
*
|
|
* @param {string[]} languages Language codes
|
|
* @return {string[]} List of language codes supported by the article
|
|
*/
|
|
function filterByBabelLanguages( languages ) {
|
|
var babelLanguages = mw.config.get( 'wgULSBabelLanguages' ) || [];
|
|
|
|
return babelLanguages.filter( function ( language ) {
|
|
return languages.indexOf( language ) >= 0;
|
|
} );
|
|
}
|
|
|
|
/**
|
|
* Filter the language list by site picks.
|
|
*
|
|
* @param {string[]} languages Language codes
|
|
* @return {string[]} List of language codes supported by the article
|
|
*/
|
|
function filterBySitePicks( languages ) {
|
|
var picks = mw.config.get( 'wgULSCompactLinksPrepend' ) || [];
|
|
return picks.filter( function ( language ) {
|
|
return languages.indexOf( language ) >= 0;
|
|
} );
|
|
}
|
|
|
|
/**
|
|
* Filter the language list by common languages.
|
|
* Common languages are the most probable languages predicted by ULS.
|
|
*
|
|
* @param {string[]} languages Language codes
|
|
* @return {string[]} List of language codes supported by the article
|
|
*/
|
|
function filterByCommonLanguages( languages ) {
|
|
var commonLanguages = mw.uls.getFrequentLanguageList();
|
|
|
|
return commonLanguages.filter( function ( language ) {
|
|
return languages.indexOf( language ) >= 0;
|
|
} );
|
|
}
|
|
|
|
/**
|
|
* Filter the language list by globally common languages, i.e.
|
|
* this list is not user specific.
|
|
*
|
|
* @param {string[]} languages Language codes
|
|
* @return {string[]} List of language codes supported by the article
|
|
*/
|
|
function getExtraCommonLanguages( languages ) {
|
|
var commonLanguages = [
|
|
'zh', 'en', 'hi', 'ur', 'es', 'ar', 'ru', 'id', 'ms', 'pt',
|
|
'fr', 'de', 'bn', 'ja', 'pnb', 'pa', 'jv', 'te', 'ta', 'ko', 'mr', 'tr', 'vi',
|
|
'it', 'fa', 'sv', 'nl', 'pl'
|
|
];
|
|
|
|
return commonLanguages.filter( function ( language ) {
|
|
return languages.indexOf( language ) >= 0;
|
|
} );
|
|
}
|
|
|
|
/**
|
|
* Filter the language list by Translate's assistant languages.
|
|
* Where available, they're languages deemed useful by the user.
|
|
*
|
|
* @param {string[]} languages Language codes
|
|
* @return {string[]} List of language codes supported by the article
|
|
*/
|
|
function filterByAssistantLanguages( languages ) {
|
|
var assistantLanguages = mw.user.options.get( 'translate-editlangs' );
|
|
if ( !assistantLanguages || assistantLanguages === 'default' ) {
|
|
return [];
|
|
}
|
|
|
|
return assistantLanguages.split( /,\s*/ ).filter( function ( language ) {
|
|
return languages.indexOf( language ) >= 0;
|
|
} );
|
|
}
|
|
|
|
/**
|
|
* @class
|
|
* @constructor
|
|
* @param {HTMLElement} listElement Interlanguage list element
|
|
* @param {Object} options
|
|
*/
|
|
function CompactInterlanguageList( listElement, options ) {
|
|
this.listElement = listElement;
|
|
this.options = options || {};
|
|
|
|
/**
|
|
* @private
|
|
* @property {Object} interlanguageList
|
|
*/
|
|
this.interlanguageList = null;
|
|
|
|
/**
|
|
* @private
|
|
* @property {Object} interlanguageList
|
|
*/
|
|
this.compactList = null;
|
|
|
|
this.commonInterlanguageList = null;
|
|
this.$trigger = null;
|
|
this.compactSize = 0;
|
|
this.listSize = 0;
|
|
}
|
|
|
|
/**
|
|
* Initialize the plugin
|
|
*/
|
|
CompactInterlanguageList.prototype.init = function () {
|
|
var self = this,
|
|
max = this.options.max || DEFAULT_LIST_SIZE;
|
|
|
|
this.interlanguageList = this.getInterlanguageList();
|
|
this.listSize = Object.keys( this.interlanguageList ).length;
|
|
|
|
if ( this.listSize <= max ) {
|
|
// Not enough languages to compact the list
|
|
mw.hook( 'mw.uls.compactlinks.initialized' ).fire( false );
|
|
return;
|
|
}
|
|
|
|
// If we're only a bit beyond max, limit to 7 instead of 9.
|
|
// FIXME: This assumes the max is 9.
|
|
self.compactSize = ( self.listSize <= 12 ) ? 7 : max;
|
|
self.compactList = self.getCompactList();
|
|
self.hideOriginal();
|
|
self.render();
|
|
self.listen();
|
|
};
|
|
|
|
/**
|
|
* Render the compacted interlanguage list and triggers
|
|
*/
|
|
CompactInterlanguageList.prototype.render = function () {
|
|
var language;
|
|
|
|
for ( language in this.compactList ) {
|
|
this.compactList[ language ].element.parentNode.style.display = '';
|
|
}
|
|
|
|
this.addTrigger();
|
|
|
|
mw.hook( 'mw.uls.compactlinks.initialized' ).fire( true );
|
|
};
|
|
|
|
/**
|
|
* Attaches the actual selector to the trigger.
|
|
*
|
|
* @param {jQuery} $trigger Element to use as trigger.
|
|
*/
|
|
CompactInterlanguageList.prototype.createSelector = function ( $trigger ) {
|
|
var languages = Object.keys( this.interlanguageList ),
|
|
self = this,
|
|
ulsLanguageList = {};
|
|
|
|
$.each( this.interlanguageList, function ( languageCode, language ) {
|
|
ulsLanguageList[ languageCode ] = language.autonym;
|
|
} );
|
|
|
|
// Attach ULS to the trigger
|
|
$trigger.uls( {
|
|
onReady: function () {
|
|
this.$menu.addClass( 'interlanguage-uls-menu' );
|
|
},
|
|
/**
|
|
* Language selection handler
|
|
*
|
|
* @param {string} language language code
|
|
* @param {Object} event jQuery event object
|
|
*/
|
|
onSelect: function ( language, event ) {
|
|
self.$trigger.removeClass( 'selector-open' );
|
|
mw.uls.addPreviousLanguage( language );
|
|
|
|
// Switch the current tab to the new language,
|
|
// unless it was Ctrl-click or Command-click
|
|
if ( !event.metaKey && !event.shiftKey ) {
|
|
location.href = self.interlanguageList[ language ].href;
|
|
}
|
|
},
|
|
onVisible: function () {
|
|
var offset, height, width, triangleWidth;
|
|
// The panel is positioned carefully so that our pointy triangle,
|
|
// which is implemented as a square box rotated 45 degrees with
|
|
// rotation origin in the middle. See the corresponding style file.
|
|
|
|
// These are for the trigger
|
|
offset = $trigger.offset();
|
|
width = $trigger.outerWidth();
|
|
height = $trigger.outerHeight();
|
|
|
|
// Triangle width is: who knows now, but this still looks fine.
|
|
triangleWidth = 12;
|
|
|
|
if ( offset.left > $( window ).width() / 2 ) {
|
|
this.left = offset.left - this.$menu.outerWidth() - triangleWidth;
|
|
this.$menu.removeClass( 'selector-left' ).addClass( 'selector-right' );
|
|
} else {
|
|
this.left = offset.left + width + triangleWidth;
|
|
this.$menu.removeClass( 'selector-right' ).addClass( 'selector-left' );
|
|
}
|
|
// Offset from the middle of the trigger
|
|
this.top = offset.top + ( height / 2 ) - 27;
|
|
|
|
this.$menu.css( {
|
|
left: this.left,
|
|
top: this.top
|
|
} );
|
|
$trigger.addClass( 'selector-open' );
|
|
},
|
|
languageDecorator: function ( $languageLink, language ) {
|
|
var data = self.interlanguageList[ language ];
|
|
// Set href, text, and tooltip exactly same as what was in
|
|
// interlanguage link. The ULS autonym might be different in some
|
|
// cases like sr. In ULS it is "српски", while in interlanguage links
|
|
// it is "српски / srpski"
|
|
$languageLink
|
|
.prop( {
|
|
href: data.href,
|
|
title: data.element.title
|
|
} )
|
|
.text( data.autonym );
|
|
|
|
// This code is to support badges used in Wikimedia
|
|
$languageLink.parent().addClass( data.element.parentNode.className );
|
|
},
|
|
onCancel: function () {
|
|
$trigger.removeClass( 'selector-open' );
|
|
},
|
|
languages: ulsLanguageList,
|
|
ulsPurpose: 'compact-language-links',
|
|
// Show common languages
|
|
quickList: self.getCommonLanguages( languages ),
|
|
noResultsTemplate: function () {
|
|
var $defaultTemplate = $.fn.lcd.defaults.noResultsTemplate.call( this );
|
|
// Customize the message
|
|
$defaultTemplate
|
|
.find( '.uls-no-results-found-title' )
|
|
.data( 'i18n', 'ext-uls-compact-no-results' );
|
|
return $defaultTemplate;
|
|
}
|
|
} );
|
|
};
|
|
|
|
/**
|
|
* Bind to event handlers and listen for events
|
|
*/
|
|
CompactInterlanguageList.prototype.listen = function () {
|
|
var self = this;
|
|
|
|
this.$trigger.one( 'click', function () {
|
|
// Load the ULS now.
|
|
mw.loader.using( 'ext.uls.mediawiki' ).then( function () {
|
|
self.createSelector( self.$trigger );
|
|
self.$trigger.click();
|
|
} );
|
|
} );
|
|
};
|
|
|
|
/**
|
|
* Get the compacted interlanguage list as associative array
|
|
*
|
|
* @return {Object}
|
|
*/
|
|
CompactInterlanguageList.prototype.getCompactList = function () {
|
|
var language, languages, compactLanguages, i, compactedList;
|
|
|
|
compactedList = {};
|
|
languages = Object.keys( this.interlanguageList );
|
|
compactLanguages = this.compact( languages );
|
|
|
|
for ( i = 0; i < compactLanguages.length; i++ ) {
|
|
language = compactLanguages[ i ];
|
|
compactedList[ language ] = this.interlanguageList[ language ];
|
|
}
|
|
|
|
return compactedList;
|
|
};
|
|
|
|
/**
|
|
* Get compacting strategies.
|
|
* The items will be executed in the given order till the required
|
|
* compact size is achieved. Each item should be an array and should
|
|
* take the whole language list as argument.
|
|
*
|
|
* @return {Function[]} Array of compacting functions
|
|
*/
|
|
CompactInterlanguageList.prototype.getCompactStrategies = function () {
|
|
return [
|
|
// Add user-defined assistant languages on wikis with Translate extension.
|
|
filterByAssistantLanguages,
|
|
// Add previously selected languages.
|
|
// Previous languages are always the better suggestion
|
|
// because the user has explicitly chosen them.
|
|
filterByPreviousLanguages,
|
|
// User's languages in the Babel box on the user page
|
|
filterByBabelLanguages,
|
|
// Site specific highlights, mostly used on Wikimedia sites
|
|
filterBySitePicks,
|
|
// Add all common languages to the beginning of array.
|
|
// These are the most probable languages predicted by ULS.
|
|
this.getCommonLanguages,
|
|
// Add languages that are present in the article content.
|
|
this.filterByLangsInText,
|
|
// Add languages in which there are featured articles.
|
|
this.filterByBadges,
|
|
// Some global fallbacks to avoid showing languages in the beginning of the alphabet
|
|
getExtraCommonLanguages,
|
|
// Finally add the whole languages array too.
|
|
// We will remove duplicates and cut down to required size.
|
|
this.finalFallback
|
|
];
|
|
};
|
|
|
|
/**
|
|
* Compact a given array of languages
|
|
*
|
|
* @param {Array} languages
|
|
* @return {Array} Compacted array
|
|
*/
|
|
CompactInterlanguageList.prototype.compact = function ( languages ) {
|
|
var i, strategies,
|
|
compactLanguages = [];
|
|
|
|
strategies = this.getCompactStrategies();
|
|
for ( i = 0; i < strategies.length; i++ ) {
|
|
compactLanguages = concatWithoutDuplicates(
|
|
compactLanguages, strategies[ i ].call( this, languages )
|
|
);
|
|
if ( compactLanguages.length >= this.compactSize ) {
|
|
// We have more than enough items. Stop here.
|
|
compactLanguages = compactLanguages.slice( 0, this.compactSize );
|
|
break;
|
|
}
|
|
}
|
|
|
|
return compactLanguages;
|
|
};
|
|
|
|
/**
|
|
* Filter the language list by languages that appear in
|
|
* the page's text. This is done by looking for HTML elements with
|
|
* a "lang" attribute—they are likely to appear in a foreign name,
|
|
* for example.
|
|
*
|
|
* The reader doesn't necessarily know this language, but it
|
|
* appears relevant to the page.
|
|
*
|
|
* @param {string[]} languages Language codes
|
|
* @return {string[]} List of language codes supported by the article
|
|
*/
|
|
CompactInterlanguageList.prototype.filterByLangsInText = function ( languages ) {
|
|
var languagesInText = [];
|
|
$.each( document.querySelectorAll( '#mw-content-text [lang]' ), function ( i, el ) {
|
|
var lang = convertMediaWikiLanguageCodeToULS( el.lang );
|
|
if ( languagesInText.indexOf( lang ) === -1 && languages.indexOf( lang ) >= 0 ) {
|
|
languagesInText.push( lang );
|
|
}
|
|
} );
|
|
|
|
return languagesInText;
|
|
};
|
|
|
|
/**
|
|
* Filter the language list by languages the page in which
|
|
* has any kind of a badge, such as "featured article".
|
|
* The "badge-*" classes are added by Wikibase.
|
|
*
|
|
* The reader doesn't necessarily know this language, but it
|
|
* appears relevant to the page.
|
|
*
|
|
* @return {Array} List of language codes in which there are articles with badges
|
|
*/
|
|
CompactInterlanguageList.prototype.filterByBadges = function () {
|
|
return $.map(
|
|
document.querySelectorAll( '#p-lang [class*="badge"]' ),
|
|
function ( el ) {
|
|
return convertMediaWikiLanguageCodeToULS(
|
|
el.querySelector( '.interlanguage-link-target' ).lang
|
|
);
|
|
}
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Find out the existing languages supported
|
|
* by the article and fetch their href.
|
|
*
|
|
* @return {Object} List of existing language codes and their hrefs
|
|
*/
|
|
CompactInterlanguageList.prototype.getInterlanguageList = function () {
|
|
var interlanguageList = {};
|
|
|
|
$.each( this.listElement.querySelectorAll( '.interlanguage-link-target' ), function ( i, el ) {
|
|
var langCode = convertMediaWikiLanguageCodeToULS( el.lang );
|
|
interlanguageList[ langCode ] = {
|
|
href: el.href,
|
|
autonym: el.textContent,
|
|
element: el
|
|
};
|
|
} );
|
|
|
|
return interlanguageList;
|
|
};
|
|
|
|
/**
|
|
* Get common languages - the most probable languages predicted by ULS.
|
|
*
|
|
* @param {string[]} languages Language codes
|
|
* @return {string[]} List of all common language codes
|
|
*/
|
|
CompactInterlanguageList.prototype.getCommonLanguages = function ( languages ) {
|
|
if ( this.commonInterlanguageList === null ) {
|
|
this.commonInterlanguageList = filterByCommonLanguages( languages );
|
|
}
|
|
|
|
return this.commonInterlanguageList;
|
|
};
|
|
|
|
CompactInterlanguageList.prototype.finalFallback = function ( languages ) {
|
|
return languages;
|
|
};
|
|
|
|
/**
|
|
* Hide the original interlanguage list
|
|
*/
|
|
CompactInterlanguageList.prototype.hideOriginal = function () {
|
|
var links = this.listElement.querySelectorAll( '.interlanguage-link' ),
|
|
i = links.length;
|
|
while ( i-- ) {
|
|
links[ i ].style.display = 'none';
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Add the trigger at the bottom of the language list
|
|
*/
|
|
CompactInterlanguageList.prototype.addTrigger = function () {
|
|
var trigger = document.createElement( 'button' );
|
|
trigger.className = 'mw-interlanguage-selector mw-ui-button';
|
|
trigger.title = mw.message( 'ext-uls-compact-link-info' ).plain();
|
|
// Use text() because the message needs {{PLURAL:}}
|
|
trigger.textContent = mw.message(
|
|
'ext-uls-compact-link-count',
|
|
mw.language.convertNumber( this.listSize - this.compactSize )
|
|
).text();
|
|
|
|
this.listElement.appendChild( trigger );
|
|
this.$trigger = $( trigger );
|
|
};
|
|
|
|
/**
|
|
* Performance cost of calling createCompactList(), as of 2018-09-10.
|
|
*
|
|
* Summary:
|
|
* - DOM Queries: 5 + 1N
|
|
* * createCompactList (1 querySelector)
|
|
* * filterByBadges (1N querySelector, 1 querySelectorAll)
|
|
* * getInterlanguageList (1 querySelectorAll)
|
|
* * filterByLangsInText (1 querySelectorAll)
|
|
* * hideOriginal (1 querySelectorAll)
|
|
* - DOM Writes: 1 + 2N
|
|
* * addTrigger (1 appendChild)
|
|
* * hideOriginal (1N Element.style)
|
|
* * render (1N Element.style)
|
|
* - Misc: 1
|
|
* * addTrigger (1 mw.Message#parser)
|
|
*/
|
|
function createCompactList() {
|
|
var listElement, compactList;
|
|
listElement = document.querySelector( '#p-lang ul' );
|
|
if ( !listElement ) {
|
|
// Not all namespaces/pages/actions have #p-lang.
|
|
return;
|
|
}
|
|
compactList = new CompactInterlanguageList( listElement, {
|
|
// Compact the list to this size
|
|
max: 9
|
|
} );
|
|
compactList.init();
|
|
|
|
}
|
|
|
|
// Early execute of createCompactList
|
|
if ( document.readyState === 'interactive' ) {
|
|
createCompactList();
|
|
} else {
|
|
$( createCompactList );
|
|
}
|
|
|
|
}() );
|