Files
mediawiki-extensions-Univer…/resources/js/ext.uls.compactlinks.js
Timo Tijhof fcfa36ac77 compactlinks: Optimise performance of list creation logic
Add potential items directly to a single array with two indexOf
checks.

Previously, potential items were first put in a new temporary array,
then that array was filtered with indexOf checks into another temporary
array, and then the result of that combined with the previous results
into another temporary filtered array, and then the previous results
array replaced with the new one. This eliminates 2*2N filter+concat
calls an their overhead from calling functions, creating arrays,
copying arrays and the memory for those arrays. (Where N is the number
of language-list strategies). It also reduces the total number of
indexOf calls.

Below is a comparison of time spent in createCompactList() during
page load process, compared to master without this and the previous
commit. Measured on localhost with Vector, EventLogging, Interwiki,
and ULS installed; on a page that is a copy of en.wikipedia.org/Messier_87
and its 51 interlanguage links. This page was a featured article
last week. Its count of links seems average compared to other
featured and/or most-viewed articles last week (the other ones
I checked had 23, 43, 52, and 76 langlinks respectively).

|--------------|----------|--------|
|              | Before   | After  |
|--------------|----------|--------|
| Chrome 69    | 77.5ms   | 41.4ms |
| MacBook Pro  | 57.7ms   | 44.4ms |
| CPU 1/6th    | 63.8ms   | 45.3ms |
|--------------|----------|--------|
| Firefox 61   | 12ms     | 10ms   |
|              | 10ms     | 9ms    |
|              | 11ms     | 8ms    |
|--------------|----------|--------|
| Safari 11    | 3.5ms    | 2.7ms  |
|              | 3.3ms    | 2.6ms  |
|              | 3.4ms    | 2.8ms  |
|--------------|----------|--------|

Bug: T127328
Change-Id: I56052e7c01c6a667500773e12c755a7a5f5d9cd0
2018-09-24 17:19:45 +00:00

548 lines
16 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;
/**
* @param {Array} target
* @param {Array} source
* @param {string|string[]|undefined} items Language code, or list of language codes
*/
function addMatchWithoutDuplicate( target, source, items ) {
var i;
if ( items === undefined ) {
return;
}
items = !Array.isArray( items ) ? [ items ] : items;
for ( i = 0; i < items.length; i++ ) {
if (
// Only add if unique and matches source
target.indexOf( items[ i ] ) === -1 &&
source.indexOf( items[ i ] ) !== -1
) {
target.push( items[ i ] );
}
}
}
/**
* 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;
}
/**
* Get user-defined assistant languages on wikis with Translate extension.
*
* Where available, they're languages deemed useful by the user.
*
* @return {string[]|undefined} Language codes
*/
function getAssistantLanguages() {
var assistantLanguages = mw.user.options.get( 'translate-editlangs' );
if ( !assistantLanguages || assistantLanguages === 'default' ) {
return;
}
return assistantLanguages.split( /,\s*/ );
}
/**
* Get previously selected languages.
*
* Previous languages are a good suggestion because the user has
* explicitly chosen them in the past.
*
* @return {string[]} Language codes
*/
function getPreviousLanguages() {
return mw.uls.getPreviousLanguages();
}
/**
* Get languages from the Babel box on the user's user page.
*
* @return {string[]|undefined} Language codes
*/
function getBabelLanguages() {
return mw.config.get( 'wgULSBabelLanguages' );
}
/**
* Get site-specific highlighted languags. Mostly used on Wikimedia sites.
*
* @return {string[]|undefined} Language codes
*/
function getSitePicks() {
return mw.config.get( 'wgULSCompactLinksPrepend' );
}
/**
* Get probable languages predicted by ULS.
*
* @return {string[]} Language codes
*/
function getCommonLanguages() {
return mw.uls.getFrequentLanguageList();
}
/**
* Get globally common languages.
*
* These are not user-specific. This helps to avoid biasing the compact list
* to language codes that sort to the beginning of the alphabet in the
* final stage.
*
* @return {string[]} Language codes
*/
function getExtraCommonLanguages() {
return [
'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'
];
}
/**
* The final strategy is the original interlanguage list.
*
* @param {string[]} languages Language codes
* @return {string[]} Language codes
*/
function getFinalFallback( languages ) {
return languages;
}
/**
* @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 strategy is given two arrays: `candidates`
* and `languages`. The candidates array is a list the callback should add to.
* The languages list contains language codes actually available for the current
* page, the callback may use this to optimise their search for candidates,
* although compact() will filter out irrelevant candidates so strategies should
* only use this if it helps narrow their search for candidates, avoid needless
* filtering that compact() will do already.
*
* @return {Function[]} Array of compacting functions
*/
CompactInterlanguageList.prototype.getCompactStrategies = function () {
return [
getAssistantLanguages,
getPreviousLanguages,
getBabelLanguages,
getSitePicks,
getCommonLanguages,
this.getLangsInText,
this.getLangsWithBadges,
getExtraCommonLanguages,
getFinalFallback
];
};
/**
* Compact a given array of languages
*
* @param {Array} languages
* @return {Array} Compacted array
*/
CompactInterlanguageList.prototype.compact = function ( languages ) {
var i, strategies, found,
compactLanguages = [];
strategies = this.getCompactStrategies();
for ( i = 0; i < strategies.length; i++ ) {
found = strategies[ i ]( languages );
// Add language codes from 'found' that are also in 'languages'
// to 'compactLanguages' (if not already in there).
addMatchWithoutDuplicate( compactLanguages, languages, found );
if ( compactLanguages.length >= this.compactSize ) {
// We have more than enough items. Stop here.
compactLanguages = compactLanguages.slice( 0, this.compactSize );
break;
}
}
return compactLanguages;
};
/**
* Get language codes that are used in the page's text content.
*
* 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.
*
* @return {string[]} Language codes
*/
CompactInterlanguageList.prototype.getLangsInText = function () {
var languagesInText = [];
$.each( document.querySelectorAll( '#mw-content-text [lang]' ), function ( i, el ) {
var lang = convertMediaWikiLanguageCodeToULS( el.lang );
if ( languagesInText.indexOf( lang ) === -1 ) {
languagesInText.push( lang );
}
} );
return languagesInText;
};
/**
* Get languages in which a related page 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 {string[]} Language codes
*/
CompactInterlanguageList.prototype.getLangsWithBadges = 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 = mw.uls.getFrequentLanguageList()
.filter( function ( language ) {
return languages.indexOf( language ) >= 0;
} );
}
return this.commonInterlanguageList;
};
/**
* 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)
* * getLangsWithBadges (1N querySelector, 1 querySelectorAll)
* * getInterlanguageList (1 querySelectorAll)
* * getLangsInText (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 );
}
}() );