This refactor will allow us to separate these two functions so that the modernized version of Vector can avoid loading the compact language list. Changes: * `options` is now optional to the CompactInterlanguageList constructor The max value is already defined in CompactInterlanguageList.prototype.init * this.interlanguageList is now initialized inside the constructor Previously calling CompactInterlanguageList.prototype.createSelector or getCompactList before calling init would throw an exception. This will be important in I0518ecdf402ebf5eb6bad2c430f6462322c0d8e1 when the responsibilities of wiring up the button and compacting the languages is separated. Bug: T264824 Change-Id: I9606df30a050d0cdaf7add2deff849cd5b895bab
434 lines
12 KiB
JavaScript
434 lines
12 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,
|
|
launchULS = require( './ext.uls.launch.js' );
|
|
|
|
/**
|
|
* @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 ] );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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]
|
|
* @param {Number} [options.max] maximum number of languages to show
|
|
* in the compacted list. This defaults to DEFAULT_LIST_SIZE.
|
|
*/
|
|
function CompactInterlanguageList( listElement, options ) {
|
|
this.listElement = listElement;
|
|
this.options = options || {};
|
|
|
|
/**
|
|
* @private
|
|
* @property {Object} interlanguageList
|
|
*/
|
|
this.interlanguageList = mw.uls.getInterlanguageListFromNodes(
|
|
listElement.querySelectorAll( '.interlanguage-link-target' )
|
|
);
|
|
|
|
/**
|
|
* @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 max = this.options.max || DEFAULT_LIST_SIZE;
|
|
|
|
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.
|
|
this.compactSize = ( this.listSize <= 12 ) ? 7 : max;
|
|
this.compactList = this.getCompactList();
|
|
this.hideOriginal();
|
|
this.render();
|
|
this.listen();
|
|
};
|
|
|
|
/**
|
|
* Render the compacted interlanguage list and triggers
|
|
*/
|
|
CompactInterlanguageList.prototype.render = function () {
|
|
var language;
|
|
|
|
for ( language in this.compactList ) {
|
|
this.compactList[ language ].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 ) {
|
|
launchULS(
|
|
$trigger,
|
|
this.interlanguageList
|
|
);
|
|
};
|
|
|
|
/**
|
|
* 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.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 = [];
|
|
Array.prototype.forEach.call( document.querySelectorAll( '#mw-content-text [lang]' ), function ( el ) {
|
|
var lang = mw.uls.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.
|
|
*
|
|
* @return {string[]} Language codes
|
|
*/
|
|
CompactInterlanguageList.prototype.getLangsWithBadges = function () {
|
|
return Array.prototype.map.call(
|
|
document.querySelectorAll( '#p-lang [class*="badge"]' ),
|
|
function ( el ) {
|
|
return mw.uls.convertMediaWikiLanguageCodeToULS(
|
|
el.querySelector( '.interlanguage-link-target' ).lang
|
|
);
|
|
}
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Get the list of languages links.
|
|
*
|
|
* @return {Object} Map of language codes to elements.
|
|
*/
|
|
CompactInterlanguageList.prototype.getInterlanguageList = function () {
|
|
return this.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 languages in the interlanguage list.
|
|
*
|
|
* The most relevant ones are unhidden in #render.
|
|
*/
|
|
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 );
|
|
compactList.init();
|
|
|
|
}
|
|
|
|
// Early execute of createCompactList
|
|
if ( document.readyState === 'interactive' ) {
|
|
createCompactList();
|
|
} else {
|
|
$( createCompactList );
|
|
}
|
|
|
|
}() );
|