Files
mediawiki-extensions-Univer…/resources/js/ext.uls.compactlinks.js
Ed Sanders e128c51356 build: Update eslint-config-wikimedia to 0.16.2
Change-Id: Iea4b73bdceb66e113b7f9c9244ae6b37a3ce205f
2020-07-09 16:40:05 +01:00

543 lines
15 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 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.
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 ) {
var languageCode,
languages = Object.keys( this.interlanguageList ),
self = this,
ulsLanguageList = {};
for ( languageCode in this.interlanguageList ) {
ulsLanguageList[ languageCode ] = this.interlanguageList[ languageCode ].textContent;
}
// 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 element = 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: element.href,
title: element.title
} )
.text( element.textContent );
// This code is to support badges used in Wikimedia
// eslint-disable-next-line mediawiki/class-doc
$languageLink.parent().addClass( 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.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 = 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 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 () {
var interlanguageList = {};
Array.prototype.forEach.call( this.listElement.querySelectorAll( '.interlanguage-link-target' ), function ( el ) {
var langCode = convertMediaWikiLanguageCodeToULS( el.lang );
interlanguageList[ langCode ] = 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 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, {
// Compact the list to this size
max: 9
} );
compactList.init();
}
// Early execute of createCompactList
if ( document.readyState === 'interactive' ) {
createCompactList();
} else {
$( createCompactList );
}
}() );