MediaWiki

Difference between revisions of "Common.js"

(correction du CSV, correction bug quand on sélectionne seulement une langue avec de nombreux locuteurs (comme le français))
(corrections sur la cinématique des sélecteurs, le bouton par défaut est en ordre aléatoire, +bouton 'derniers enregistrements')
Line 188: Line 188:
 
fr: 'Pas de résultat.',
 
fr: 'Pas de résultat.',
 
//qqq: 'Message when there are no results.',
 
//qqq: 'Message when there are no results.',
 +
},
 +
'msg-other-records': {
 +
en: ' (and many other results)',
 +
de: ' (und mehr anderen Ergebnisse)',
 +
fr: ' (et bien d’autres résultats)',
 +
//qqq: 'Message when there are more than 100 records.',
 
},
 
},
 
'button-gosearch': {
 
'button-gosearch': {
Line 194: Line 200:
 
fr: 'Rechercher',
 
fr: 'Rechercher',
 
//qqq: 'Button where the user clicks to search.',
 
//qqq: 'Button where the user clicks to search.',
 +
},
 +
'button-gosearch-last-records': {
 +
en: 'Last records',
 +
de: 'Letzen Aufnahmen',
 +
fr: 'Derniers enregistrements',
 +
//qqq: 'Button where the user clicks to search the last records.',
 
},
 
},
 
'button-resetsearch': {
 
'button-resetsearch': {
Line 238: Line 250:
  
 
request = {
 
request = {
'speaker': '',
 
'gender': '',
 
'language': '',
 
'proficiency': '',
 
};
 
lastRequest = {
 
 
'speaker': '',
 
'speaker': '',
 
'gender': '',
 
'gender': '',
Line 369: Line 375:
  
 
// If there are at least one unknown value, reset the selector
 
// If there are at least one unknown value, reset the selector
if( values.filter( function( x ) { return x === undefined; } ).length ) {
+
else if( values.filter( function( x ) { return x === undefined; } ).length ) {
 
values = list[type];
 
values = list[type];
 
console.debug( 'updateSelector('+type+','+force+')('+(values?values.length:'null')+' values): abort2' );
 
console.debug( 'updateSelector('+type+','+force+')('+(values?values.length:'null')+' values): abort2' );
Line 399: Line 405:
 
}
 
}
  
if( widgetType === 'ComboBoxInputWidget' && values.length === 1 ) {
+
if( widgetType === 'ComboBoxInputWidget' && values.length === 1 && automaticField[type] !== false ) {
 
oouiSelectors[type].setValue( values[0][ type + 'Label' ].value + " (" + values[0][type].value.substr( 31 ) + ")" );
 
oouiSelectors[type].setValue( values[0][ type + 'Label' ].value + " (" + values[0][type].value.substr( 31 ) + ")" );
 
// For dropdown, when there is one real value, there is also the empty value as first option
 
// For dropdown, when there is one real value, there is also the empty value as first option
} else if( widgetType === 'DropdownWidget' && values.length === 2 ) {
+
} else if( widgetType === 'DropdownWidget' && values.length === 2 && automaticField[type] !== false ) {
 
oouiSelectors[type].getMenu().selectItemByData( values[1][type].value.substr( 31 ) );
 
oouiSelectors[type].getMenu().selectItemByData( values[1][type].value.substr( 31 ) );
 
}
 
}
Line 443: Line 449:
 
function onChange( type ) {
 
function onChange( type ) {
  
// Logique ternaire pour les sélecteurs :
+
// Logique ternaire pour les sélecteurs :
 
// - null := sélecteur indéterminé, pouvant être réduit sous l’influence d’autres sélecteurs
 
// - null := sélecteur indéterminé, pouvant être réduit sous l’influence d’autres sélecteurs
 
// - false := sélecteur manuel, n’étant jamais réduit sous l’influence d’autres sélecteurs et étant toujours étendus (=comprend toutes les valeurs possibles)
 
// - false := sélecteur manuel, n’étant jamais réduit sous l’influence d’autres sélecteurs et étant toujours étendus (=comprend toutes les valeurs possibles)
Line 457: Line 463:
 
var nbFilled = 1, emptyValue = true;
 
var nbFilled = 1, emptyValue = true;
 
if( state[type] ) {
 
if( state[type] ) {
if( state[type] !== request[type] ) {
+
if( state[type] != request[type] ) {
 
automaticField[type] = false;
 
automaticField[type] = false;
 
console.debug( 'onChange('+type+'): on passe en manuel' );
 
console.debug( 'onChange('+type+'): on passe en manuel' );
Line 468: Line 474:
 
+ ( automaticField.gender === false && state.gender ? 1 : 0 )
 
+ ( automaticField.gender === false && state.gender ? 1 : 0 )
 
+ ( automaticField.proficiency === false && state.proficiency ? 1 : 0 );
 
+ ( automaticField.proficiency === false && state.proficiency ? 1 : 0 );
if( nbFilled > 1 ) {
+
if( nbFilled >= 1 ) {
 
automaticField[type] = false;
 
automaticField[type] = false;
 
emptyValue = true;
 
emptyValue = true;
if( type !== 'speaker' && !state.speaker && automaticField.speaker === true ) {
+
if( type !== 'speaker' && !state.speaker && automaticField.speaker !== false ) {
 
automaticField.speaker = emptyValue;
 
automaticField.speaker = emptyValue;
 
}
 
}
if( type !== 'gender' && !state.gender && automaticField.gender === true ) {
+
if( type !== 'gender' && !state.gender && automaticField.gender !== false ) {
 
automaticField.gender = emptyValue;
 
automaticField.gender = emptyValue;
 
}
 
}
if( type !== 'language' && !state.language && automaticField.language === true ) {
+
if( type !== 'language' && !state.language && automaticField.language !== false ) {
 
automaticField.language = emptyValue;
 
automaticField.language = emptyValue;
 
}
 
}
if( type !== 'proficiency' && !state.proficiency && automaticField.proficiency === true ) {
+
if( type !== 'proficiency' && !state.proficiency && automaticField.proficiency !== false ) {
 
automaticField.proficiency = emptyValue;
 
automaticField.proficiency = emptyValue;
 
}
 
}
Line 499: Line 505:
 
}
 
}
 
}
 
}
console.debug( nbFilled, state[type], emptyValue, automaticField );
+
console.debug( nbFilled, state[type], request[type], emptyValue, automaticField );
  
 
// Do not reduce fields above one change by the user, else it leads to a bad User Experience
 
// Do not reduce fields above one change by the user, else it leads to a bad User Experience
Line 510: Line 516:
 
var potentialSpeakers = speakersCriteria;
 
var potentialSpeakers = speakersCriteria;
  
if( state.language ) {
+
if( state.language && automaticField.language === false ) {
 
potentialSpeakers = reduceSpeakers( 2, state.language, potentialSpeakers );
 
potentialSpeakers = reduceSpeakers( 2, state.language, potentialSpeakers );
 
}
 
}
if( state.speaker ) {
+
if( state.speaker && automaticField.speaker === false ) {
 
potentialSpeakers = reduceSpeakers( 0, state.speaker, potentialSpeakers );
 
potentialSpeakers = reduceSpeakers( 0, state.speaker, potentialSpeakers );
 
}
 
}
if( state.gender ) {
+
if( state.gender && automaticField.gender === false ) {
 
potentialSpeakers = reduceSpeakers( 1, state.gender, potentialSpeakers );
 
potentialSpeakers = reduceSpeakers( 1, state.gender, potentialSpeakers );
 
}
 
}
if( state.proficiency ) {
+
if( state.proficiency && automaticField.proficiency === false ) {
 
potentialSpeakers = reduceSpeakers( 3, state.proficiency, potentialSpeakers );
 
potentialSpeakers = reduceSpeakers( 3, state.proficiency, potentialSpeakers );
 
}
 
}
  
var listSpeaker = automaticField.speaker !== false ? getListSelectorFromQids( 'speaker', dedupe( potentialSpeakers.map( function( x ) { return x[0]; } ) ) ) : null;
+
var listSpeaker = getListSelectorFromQids( 'speaker', dedupe( potentialSpeakers.map( function( x ) { return x[0]; } ) ) );
var listGender = automaticField.gender !== false ? getListSelectorFromQids( 'gender', dedupe( potentialSpeakers.map( function( x ) { return x[1]; } ) ) ) : null;
+
var listGender = getListSelectorFromQids( 'gender', dedupe( potentialSpeakers.map( function( x ) { return x[1]; } ) ) );
var listLanguage = automaticField.language !== false ? getListSelectorFromQids( 'language', dedupe( potentialSpeakers.map( function( x ) { return x[2]; } ) ) ) : null;
+
var listLanguage = getListSelectorFromQids( 'language', dedupe( potentialSpeakers.map( function( x ) { return x[2]; } ) ) );
var listProficiency = automaticField.proficiency !== false ? getListSelectorFromQids( 'proficiency', dedupe( potentialSpeakers.map( function( x ) { return x[3]; } ) ) ) : null;
+
var listProficiency = getListSelectorFromQids( 'proficiency', dedupe( potentialSpeakers.map( function( x ) { return x[3]; } ) ) );
  
 
// Update the selectors with the restricted values
 
// Update the selectors with the restricted values
Line 577: Line 583:
  
 
function reset() {
 
function reset() {
lastRequest = {
+
request = {
 
speaker: '',
 
speaker: '',
 
gender: '',
 
gender: '',
 
language: '',
 
language: '',
 
proficiency: '',
 
proficiency: '',
 +
};
 +
automaticField = {
 +
'speaker': null,
 +
'gender': null,
 +
'language': null,
 +
'proficiency': null,
 
};
 
};
 
updateSelector( 'speaker', true )( list.speaker );
 
updateSelector( 'speaker', true )( list.speaker );
Line 601: Line 613:
 
( new OO.ui.ButtonInputWidget( {
 
( new OO.ui.ButtonInputWidget( {
 
label: messages['button-gosearch'][userLanguage] ? messages['button-gosearch'][userLanguage] : messages['button-gosearch'].en
 
label: messages['button-gosearch'][userLanguage] ? messages['button-gosearch'][userLanguage] : messages['button-gosearch'].en
} ) ).on( 'click', doQuery ).$element
+
} ) ).on( 'click', factoryDoQuery( 'random' ) ).$element
 +
);
 +
 
 +
$( '#sndlib-gosearch-last-records' ).html('').append(
 +
( new OO.ui.ButtonInputWidget( {
 +
label: messages['button-gosearch-last-records'][userLanguage] ? messages['button-gosearch-last-records'][userLanguage] : messages['button-gosearch-last-records'].en
 +
} ) ).on( 'click', factoryDoQuery( 'last' ) ).$element
 
);
 
);
  
Line 685: Line 703:
 
}
 
}
 
if( moreResults ) {
 
if( moreResults ) {
$( '#sndlib-audioPages' ).append( ' (et bien d’autres résultats)' );
+
$( '#sndlib-audioPages' ).append( messages['msg-other-records'][userLanguage] ? messages['msg-other-records'][userLanguage] : messages['msg-other-records'].en );
 
}
 
}
 
}
 
}
Line 706: Line 724:
  
 
// Do SPARQL request from filters
 
// Do SPARQL request from filters
function doQuery() {
+
function factoryDoQuery( order ) {
  
request = {
+
return function doQuery() {
speaker: getQValue( 'speaker' ),
 
gender: getQValue( 'gender' ),
 
language: getQValue( 'language' ),
 
proficiency: getQValue( 'proficiency' ),
 
};
 
if( JSON.stringify( request ) === JSON.stringify( lastRequest ) ) {
 
return;
 
}
 
  
lastRequest = request;
+
request = {
 +
speaker: getQValue( 'speaker' ),
 +
gender: getQValue( 'gender' ),
 +
language: getQValue( 'language' ),
 +
proficiency: getQValue( 'proficiency' ),
 +
order: order,
 +
};
  
// When all fields are empty, do not query
+
var potentialSpeakers = speakersCriteria;
if( !request.speaker && !request.gender && !request.language && !request.proficiency ) {
 
return;
 
}
 
  
var potentialSpeakers = speakersCriteria;
+
var query = "SELECT ?record ?date WHERE { ?record prop:P2 entity:Q2 ; prop:P4 ?language ; prop:P5 ?speaker ; prop:P6 ?date . ";
 
+
if( request.language ) {
var query = "SELECT ?record ?speaker ?language WHERE { ?record prop:P2 entity:Q2 ; prop:P4 ?language ; prop:P5 ?speaker . ";
+
query += "?record prop:P4 entity:" + request.language + " . ";
if( request.language ) {
+
potentialSpeakers = reduceSpeakers( 2, request.language, potentialSpeakers );
query += "?record prop:P4 entity:" + request.language + " . ";
+
}
potentialSpeakers = reduceSpeakers( 2, request.language, potentialSpeakers );
+
if( request.speaker ) {
}
+
query += "?record prop:P5 entity:" + request.speaker + " . ";
if( request.speaker ) {
+
potentialSpeakers = reduceSpeakers( 0, request.speaker, potentialSpeakers );
query += "?record prop:P5 entity:" + request.speaker + " . ";
+
}
potentialSpeakers = reduceSpeakers( 0, request.speaker, potentialSpeakers );
+
if( request.gender ) {
}
+
potentialSpeakers = reduceSpeakers( 1, request.gender, potentialSpeakers );
if( request.gender ) {
+
}
potentialSpeakers = reduceSpeakers( 1, request.gender, potentialSpeakers );
+
if( request.proficiency ) {
}
+
potentialSpeakers = reduceSpeakers( 3, request.proficiency, potentialSpeakers );
if( request.proficiency ) {
+
query += "?speaker llp:P4 [ llv:P4 ?language ; llq:P16 entity:" + request.proficiency + " ] . ";
potentialSpeakers = reduceSpeakers( 3, request.proficiency, potentialSpeakers );
+
}
query += "?speaker llp:P4 [ llv:P4 ?language ; llq:P16 entity:" + request.proficiency + " ] . ";
+
if( potentialSpeakers.length < speakersCriteria.length ) {
}
+
var listSpeakers = dedupe( potentialSpeakers.map( function( x ) {
if( potentialSpeakers.length < speakersCriteria.length ) {
+
return 'entity:' + x[0];
var listSpeakers = dedupe( potentialSpeakers.map( function( x ) {
+
} ) ).join();
return 'entity:' + x[0];
+
console.debug( speakersCriteria, potentialSpeakers, listSpeakers );
} ) ).join();
+
if( listSpeakers.length < 70 ) {
console.debug( speakersCriteria, potentialSpeakers, listSpeakers );
+
query += 'FILTER( ?speaker IN (' + listSpeakers + ') )';
if( listSpeakers.length < 70 ) {
+
}
query += 'FILTER( ?speaker IN (' + listSpeakers + ') )';
+
}
 +
query += '}';
 +
if( order === 'random' ) {
 +
query += ' ORDER BY RAND()';
 +
} else if( order === 'last' ) {
 +
query += ' ORDER BY DESC(?date)';
 +
}
 +
query += ' LIMIT ' + (nbTotalResults+1);
 +
if( order === 'random' ) {
 +
query += '\n# ' + Date.now();
 
}
 
}
}
 
query += "} LIMIT " + (nbTotalResults+1);
 
  
// Voir https://commons.wikimedia.org/wiki/Category:Throbbers - il faut que le fond soit transparent
+
// Voir https://commons.wikimedia.org/wiki/Category:Throbbers - il faut que le fond soit transparent
$( '#sndlib-audioresults' ).html( '' ).append( '<img src="https://upload.wikimedia.org/wikipedia/commons/7/7a/Balls.gif" width="50" />' );
+
$( '#sndlib-audioresults' ).html( '' ).append( '<img src="https://upload.wikimedia.org/wikipedia/commons/7/7a/Balls.gif" width="50" />' );
 +
$( '#sndlib-audioPages' ).html( '' );
  
// Execute the request
+
// Execute the request
var result = $.getJSON(
+
var result = $.getJSON(
'https://lingualibre.org/bigdata/namespace/wdq/sparql',
+
'https://lingualibre.org/bigdata/namespace/wdq/sparql',
{
+
{
query: query,
+
query: query,
//Accept: 'application/sparql-results+json'
+
//Accept: 'application/sparql-results+json'
}
+
}
);
+
);
  
result.then( createAudioBoxesForSearch, displayError );
+
result.then( createAudioBoxesForSearch, displayError );
 +
}
 
}
 
}
  

Revision as of 16:22, 30 September 2021

// Replace Wikidata IDs with their [label, description]
if ( $( '.wb-external-id' ).length > 0 ) {
	mw.loader.using( 'mediawiki.ForeignApi', function() {
		$( '.wb-external-id' ).each( function() {
			if ( $( this ).attr( 'href' ).lastIndexOf( 'https://www.wikidata.org', 0 ) === 0 ) {
				var wikidataApi = new mw.ForeignApi( 'https://www.wikidata.org/w/api.php', {
						anonymous: true,
						parameters: { 'origin': '*' },
						ajax: { timeout: 10000 }
					} ),
					lang = mw.config.get( 'wgUserLanguage' ),
					node = $( this );
				wikidataApi.get( {
					'action': 'wbgetentities',
					'format': 'json',
					'ids': node.text(),
					'props': 'labels|descriptions',
					'languages': lang,
					'languagefallback': 1,
					'origin': '*'
				} ).then( function( data ) {
                                        
					var entity = data.entities[ node.text() ],
						label = ( entity.labels[ lang ] !== undefined ? entity.labels[ lang ].value + ' <i>(' + node.text() + ')</i>' : node.text() ),
						description = ( entity.descriptions[ lang ] !== undefined ? '<small>' + entity.descriptions[ lang ].value + '</small>' : '' );
					
					node.html( label + '<br>' + description )
				} );
			}
		} );
	} );
}

//Add an audio player to the audio records links in the wikibase items
const BASE_FILE_URL = 'https://commons.wikimedia.org/wiki/Special:Redirect/file?wptype=file&wpvalue=';

function playButton( audioUrl ) {
	var button = new OO.ui.ButtonWidget( {
		framed: false,
		icon: 'play',
		title: 'play'
	} );
	button.on( 'click', function() {
		var audio = new Audio( audioUrl );
		audio.play();
	} );

	return button.$element;
}

if ( $( '#P3 a.extiw' ).length > 0 ) {
    mw.loader.using( [ 'oojs-ui-widgets', 'oojs-ui.styles.icons-media' ], function() {
        $( '#P3 a.extiw' ).each( function() {
            var $node = $( this );
            $node.before( playButton( BASE_FILE_URL + $node.text() ) );
        } );
    } );
}


/**
 * Display last records on main page
 **/
var ab1, ab2;


var AudioBox = function( recordQid, $node ) {
	this.wbRecord = new mw.recordWizard.wikibase.Item( recordQid );
	
	this.$node = $node;
	this.audioNode = document.createElement( 'audio' );
	this.audioNode.preload = 'auto';

    this.api = new mw.Api();	

	this.recordQid = recordQid;
	this.langQid = null;
	this.speakerQid = null;
	
	this.label = '';
	this.media = '';
	this.lang = '';
	this.speaker = '';
	
	this.wbRecord.getFromApi( this.api ).then( this.processRecord.bind( this ), displayError );
}

AudioBox.prototype.processRecord = function() {
	this.label = this.wbRecord.getLabel( 'en' );
	this.media = 'https://commons.wikimedia.org/wiki/Special:FilePath/' + this.wbRecord.getStatements( 'P3' )[ 0 ].getValue();
	this.langQid = this.wbRecord.getStatements( 'P4' )[ 0 ].getValue();
	this.speakerQid = this.wbRecord.getStatements( 'P5' )[ 0 ].getValue();

	this.api.get( {
		action: "wbgetentities",
		format: "json",
		ids: this.langQid + '|' + this.speakerQid,
		props: "labels",
		languages: mw.config.get( 'wgUserLanguage' ) + "|en",
		languagefallback: 1,
	} ).then( this.processLabels.bind( this ), displayError );
}

AudioBox.prototype.processLabels = function( data ) {
	var langLabels;

	if ( data.entities === undefined || data.entities[ this.langQid ] === undefined || data.entities[ this.speakerQid ] === undefined ) {
		displayError( 'dataerror' );
		return;
	}
	langLabels = data.entities[ this.langQid ].labels;
	
	if ( langLabels[ mw.config.get( 'wgUserLanguage' ) ] !== undefined ) {
		this.lang = langLabels[ mw.config.get( 'wgUserLanguage' ) ].value;
	} else {
		this.lang = langLabels[ 'en' ].value;
	}
	
	this.speaker = data.entities[ this.speakerQid ].labels[ 'en' ].value;

	this.display();
}

AudioBox.prototype.display = function() {
	this.$node.find( '.ab-title' ).text( this.label );
	this.$node.find( '.ab-metadata' ).text( this.lang + ' - ' + this.speaker );
	
	this.audioNode.src = this.media;
	this.$node.find( '.ab-playbutton' ).click( this.audioNode.play.bind( this.audioNode ) );
}






function createAudioBoxes( data ) {
	if ( data.query === undefined || data.query.rwrecords === undefined || data.query.rwrecords.length < 2 ) {
		displayError( 'nodata' );
		return;
	}

	ab1 = new AudioBox( data.query.rwrecords[ 0 ], $( '.audiobox' ).eq( 0 ) );
	ab2 = new AudioBox( data.query.rwrecords[ 1 ], $( '.audiobox' ).eq( 1 ) );
}

function getLastRecords() {
    var api = new mw.Api();
	api.get( {
        action: 'query',
        format: 'json',
        list: 'rwrecords',
        rwrlimit: '2',
		rwrsort: 'pageid',
		rwrdir: 'descending',
		rwrformat: 'qid'
  } ).then( createAudioBoxes, displayError );
}

function displayError( code, error ) {
	console.warn( code, error );
}

if ( mw.config.get( 'wgPageName' ) === 'LinguaLibre:Main_Page' ) {
    mw.loader.using( [ 'mediawiki.api', 'ext.recordWizard.wikibase' ] ).then( getLastRecords );
}

// T206801 - Links to Wikipedia contain "+" (instead of "_")
if ( $( 'div#P19 .wb-external-id' ).length ) {
	$( 'div#P19 .wb-external-id' ).attr( 'href', $( 'div#P19 .wb-external-id' ).attr( 'href' ).replace( /\+/g, '_' ) );
}

// Interface for displaying contributions on [[LinguaLibre:Explore the sound library]]
$( function (){
	if( !/^LinguaLibre:Explore_the_sound_library(\/[a-z_-]+)?$/.test( mw.config.get( 'wgPageName' ) ) || mw.config.get( 'wgUserName' ) === 'Seb35' || mw.config.get( 'wgUserName' ) === 'Nicolas NALLET' || mw.config.get( 'wgUserName' ) === 'Mathis Back' ) {
		return;
	}

	// Parameters
	const nbTotalResults = 100;  // Total number of results requested to SPARQL endpoint
	const nbResultsPerPage = 10; // Number of results displayed per page

	// User messages to be translated
	const messages = {
		'msg-no-results': {
			en: 'No results.',
			de: 'Keine Ergebnisse.',
			fr: 'Pas de résultat.',
			//qqq: 'Message when there are no results.',
		},
		'msg-other-records': {
			en: '&nbsp;(and many other results)',
			de: '&nbsp;(und mehr anderen Ergebnisse)',
			fr: '&nbsp;(et bien d’autres résultats)',
			//qqq: 'Message when there are more than 100 records.',
		},
		'button-gosearch': {
			en: 'Search',
			de: 'Suchen',
			fr: 'Rechercher',
			//qqq: 'Button where the user clicks to search.',
		},
		'button-gosearch-last-records': {
			en: 'Last records',
			de: 'Letzen Aufnahmen',
			fr: 'Derniers enregistrements',
			//qqq: 'Button where the user clicks to search the last records.',
		},
		'button-resetsearch': {
			en: 'Reset',
			de: 'Löschen',
			fr: 'Effacer',
			//qqq: 'Button where the user clicks to reset the form.',
		},
		'button-datasets': {
			en: 'Datasets',
			de: 'Datensätze',
			fr: 'Jeux de données',
			//qqq: 'Button - link to datasets.',
		},
		'placeholder': {
		    en: ' Select or type',
		    fr: ' Sélectionner ou écrire',
		    //qqq: 'Placeholder for the fields.',
		},
};

	const userLanguage = mw.config.get( 'wgPageContentLanguage' );
	mw.loader.using( ['oojs', 'oojs-ui', 'mediawiki.api', 'ext.recordWizard.wikibase'], function () {

		oouiSelectors = {
			'speaker': null,
			'gender': null,
			'language': null,
			'proficiency': null,
		};
		var sparqlGlobal = {
			'speaker': 'SELECT ?speaker ?speakerLabel ?gender ?language ?proficiency WHERE { ?speaker prop:P2 entity:Q3 . OPTIONAL { ?speaker prop:P8 ?gender } . OPTIONAL { ?speaker llp:P4 ?statement . ?statement llv:P4 ?language . OPTIONAL { ?statement llq:P16 ?proficiency } } . SERVICE wikibase:label { bd:serviceParam wikibase:language "' + userLanguage + ',fr,en" } } ORDER BY ?speakerLabel',
			'gender': 'SELECT DISTINCT ?gender ?genderLabel WHERE { ?gender prop:P2 entity:Q7 . SERVICE wikibase:label { bd:serviceParam wikibase:language "' + userLanguage + ',fr,en" } } ORDER BY ?gender',
			'language': 'SELECT DISTINCT ?language ?languageLabel WHERE { ?language prop:P2 entity:Q4 . SERVICE wikibase:label { bd:serviceParam wikibase:language "' + userLanguage + ',fr,en" } } ORDER BY ?languageLabel',
			'proficiency': 'SELECT DISTINCT ?proficiency ?proficiencyLabel WHERE { ?proficiency prop:P2 entity:Q5 . SERVICE wikibase:label { bd:serviceParam wikibase:language "' + userLanguage + ',fr,en" } } ORDER BY ?proficiency',
		};
		const htmlElements = {
			'speaker': '#sndlib-filteruser',
			'gender': '#sndlib-filtergender',
			'language': '#sndlib-filterlanguage',
			'proficiency': '#sndlib-filterlevelofproficiency',
		};
		const unknown = {};

		request = {
			'speaker': '',
			'gender': '',
			'language': '',
			'proficiency': '',
		};
		speakersCriteria = [];
		list = {
			'speaker': null,
			'gender': null,
			'language': null,
			'proficiency': null,
		};
		mapping = {
			'speaker': null,
			'gender': null,
			'language': null,
			'proficiency': null,
		};
		automaticField = {
			'speaker': null,
			'gender': null,
			'language': null,
			'proficiency': null,
		};

		// Helper function to obtain the Qvalue of some selector
		function getQValue( type ) {
			var rawValue;
			if( type === 'speaker' || type === 'language' ) {
				rawValue = oouiSelectors[type].getValue();
			} else {
				rawValue = oouiSelectors[type].getMenu().findSelectedItem() ? oouiSelectors[type].getMenu().findSelectedItem().getData() : '';
			}
			rawValue = rawValue.trim();
			if( !rawValue ) {
				return null;
			} else if( /^Q[0-9]+$/.test( rawValue ) ) {
				return rawValue;
			} else if( /\((Q[0-9]+)\)$/.test( rawValue ) ) {
				return rawValue.replace( /.*\((Q[0-9]+)\)$/, '$1' );
			} else {
				return '';
			}
		}

		// Helper function to dedupe a sorted list
		function dedupe( arr, fn ) {
			if( fn ) {
				return arr.filter( function( x, i, a ) {
					return !i || fn( x ) !== fn( a[i-1] );
				} );
			}
			return arr.filter( function( x, i, a ) {
				return !i || x !== a[i-1];
			} );
		}

		// Returns the list of items of a given type for a restricted list of Qids
		function getListSelectorFromQids( type, qids ) {
			const indexes = dedupe( qids.map( function( x ) {
				return mapping[type][x];
			} ).sort( function( a, b ) { return a-b; } ) );
			const r = indexes.map( function( x ) {
				return list[type][x];
			} );
			return r;
		}

		// Initialise a selector and keep the list in memory the result for later reuse
		function updateSelector( type, force ) {
			return function( values, emptyValue ) {

				if( emptyValue === undefined ) {
					emptyValue = true;
				}

				console.debug( 'updateSelector('+type+','+force+')('+(values?values.length:'null')+' values,'+(emptyValue===true?'true':(emptyValue===null?'null':(emptyValue===false?'false':'autre')))+')' );

				const widgetType = type === 'speaker' || type === 'language' ? 'ComboBoxInputWidget' : 'DropdownWidget';

				if( !list[type] ) {

					// Save the list in a global variable to quickly filter then
					// We dedupe because the 'speaker' (only) has duplicate (ordered) values
					const v = values.results.bindings;
					list[type] = dedupe( v, function( x ) { return x[type].value; } );
					if( widgetType === 'DropdownWidget' ) {
						var emptyItem = [ {} ];
						emptyItem[0][type] = { value: '' };
						emptyItem[0][type+'Label'] = { value: ' ' };
						list[type] = emptyItem.concat( list[type] );
					}
					values = list[type];

					// Save the mapping Qxx → index to update the list from a list of Qxx (see doQuery)
					console.debug( type, list[type] );
					mapping[type] = list[type].reduce( function( o, x, i ) {
						o[ x[type].value.substr( 31 ) ] = i;
						return o;
					}, {} );

					// Save the global matrix: speaker x gender x language x proficiency
					if( type === 'speaker' ) {
						speakersCriteria = v.map( function( x ) {
							return [
								x.speaker.value.substr( 31 ),
								!x.gender || x.gender.type === 'bnode' ? unknown : x.gender.value.substr( 31 ),
								!x.language || x.language.type === 'bnode' ? unknown : x.language.value.substr( 31 ),
								!x.proficiency || x.proficiency.type === 'bnode' ? unknown : x.proficiency.value.substr( 31 ),
							];
						} );
					}
				}

				// Never update manual selectors (except if the user forces the reset)
				if( !force && getQValue( type ) && automaticField[type] === false ) {
					console.debug( 'updateSelector('+type+','+force+')('+(values?values.length:'null')+' values): abort1' );
					return;
				}

				if( force || values === null ) {
					values = list[type];
				}

				// If there are at least one unknown value, reset the selector
				else if( values.filter( function( x ) { return x === undefined; } ).length ) {
					values = list[type];
					console.debug( 'updateSelector('+type+','+force+')('+(values?values.length:'null')+' values): abort2' );
				}

				// For dropdowns, add an empty item if there is none
				if( widgetType === 'DropdownWidget' && values.length && values[0][type].value ) {
					var emptyItem = [ {} ];
					emptyItem[0][type] = { value: '' };
					emptyItem[0][type+'Label'] = { value: ' ' };
					values = emptyItem.concat( values );
				}

				// Create the OOUI selector
				oouiSelectors[type] = new OO.ui[widgetType]( {
					placeholder: messages['placeholder'][userLanguage] ? messages['placeholder'][userLanguage] : messages['placeholder'].en,
					menu: {
						filterFromInput: true,				
						items: values.map( function( x ) {
							return new OO.ui.MenuOptionWidget( {
								data: widgetType === 'ComboBoxInputWidget' ? x[ type + 'Label' ].value + " (" + x[type].value.substr( 31 ) + ")" : x[type].value.substr( 31 ),
								label: x[ type + 'Label' ].value
							} );
						} )
					}
				} );
				if( widgetType === 'DropdownWidget' && values.length > 2 ) {
					oouiSelectors[type].getMenu().items[0].toggle( false );
				}

				if( widgetType === 'ComboBoxInputWidget' && values.length === 1 && automaticField[type] !== false ) {
					oouiSelectors[type].setValue( values[0][ type + 'Label' ].value + " (" + values[0][type].value.substr( 31 ) + ")" );
				// For dropdown, when there is one real value, there is also the empty value as first option
				} else if( widgetType === 'DropdownWidget' && values.length === 2 && automaticField[type] !== false ) {
					oouiSelectors[type].getMenu().selectItemByData( values[1][type].value.substr( 31 ) );
				}

				$( htmlElements[type] ).html('').append(
					oouiSelectors[type].$element
				);
				if( !force ) {
					console.debug( '!force', type, getQValue( type ), automaticField[type] );
					if( automaticField[type] !== false ) {
						automaticField[type] = getQValue( type ) ? true : emptyValue;
					}
					console.debug( 'updateSelector('+type+','+force+')('+(values?values.length:'null')+' values): on passe en automatique' );
				} else {
					automaticField[type] = null;
					console.debug( 'updateSelector('+type+','+force+')('+(values?values.length:'null')+' values): on passe en manuel' );
				}

				if( widgetType === 'ComboBoxInputWidget' ) {
					oouiSelectors[type].on( 'change', function() {
						if( getQValue( type ) === '' ) {
							return;
						}
						onChange( type );
					} );
				} else {
					oouiSelectors[type].getMenu().on( 'select', function( item ) {
						if( item.getData() === '' ) {
							item.toggle( false );
						} else {
							oouiSelectors[type].getMenu().items[0].toggle( true );
						}
						onChange( type );
					} );
				}

			};
		}

		function onChange( type ) {

			// Logique ternaire pour les sélecteurs :
			// - null := sélecteur indéterminé, pouvant être réduit sous l’influence d’autres sélecteurs
			// - false := sélecteur manuel, n’étant jamais réduit sous l’influence d’autres sélecteurs et étant toujours étendus (=comprend toutes les valeurs possibles)
			// - true := sélecteur automatique, étant toujours réduit sous l’influence d’autres sélecteurs

			console.debug( 'onChange('+type+')' );
			var state = {
				speaker: getQValue( 'speaker' ),
				gender: getQValue( 'gender' ),
				language: getQValue( 'language' ),
				proficiency: getQValue( 'proficiency' ),
			};
			var nbFilled = 1, emptyValue = true;
			if( state[type] ) {
				if( state[type] != request[type] ) {
					automaticField[type] = false;
					console.debug( 'onChange('+type+'): on passe en manuel' );
				}
			} else {
				automaticField[type] = null;
				emptyValue = null;
				nbFilled = ( automaticField.language === false && state.language ? 1 : 0 )
					+ ( automaticField.speaker === false && state.speaker ? 1 : 0 )
					+ ( automaticField.gender === false && state.gender ? 1 : 0 )
					+ ( automaticField.proficiency === false && state.proficiency ? 1 : 0 );
				if( nbFilled >= 1 ) {
					automaticField[type] = false;
					emptyValue = true;
					if( type !== 'speaker' && !state.speaker && automaticField.speaker !== false ) {
						automaticField.speaker = emptyValue;
					}
					if( type !== 'gender' && !state.gender && automaticField.gender !== false ) {
						automaticField.gender = emptyValue;
					}
					if( type !== 'language' && !state.language && automaticField.language !== false ) {
						automaticField.language = emptyValue;
					}
					if( type !== 'proficiency' && !state.proficiency && automaticField.proficiency !== false ) {
						automaticField.proficiency = emptyValue;
					}
				} else {
					emptyValue = null;
					if( !state.speaker ) {
						automaticField.speaker = emptyValue;
					}
					if( !state.gender ) {
						automaticField.gender = emptyValue;
					}
					if( !state.language ) {
						automaticField.language = emptyValue;
					}
					if( !state.proficiency ) {
						automaticField.proficiency = emptyValue;
					}
				}
			}
			console.debug( nbFilled, state[type], request[type], emptyValue, automaticField );

			// Do not reduce fields above one change by the user, else it leads to a bad User Experience
			// because fields change themselves with a hardly-understandable logic
			//var nbFilled = ( state.language ? 1 : 0 ) + ( state.speaker ? 1 : 0 ) + ( state.gender ? 1 : 0 ) + ( state.proficiency ? 1 : 0 );
			//if( nbFilled > 1 ) {
			//	return;
			//}

			var potentialSpeakers = speakersCriteria;

			if( state.language && automaticField.language === false ) {
				potentialSpeakers = reduceSpeakers( 2, state.language, potentialSpeakers );
			}
			if( state.speaker && automaticField.speaker === false ) {
				potentialSpeakers = reduceSpeakers( 0, state.speaker, potentialSpeakers );
			}
			if( state.gender && automaticField.gender === false ) {
				potentialSpeakers = reduceSpeakers( 1, state.gender, potentialSpeakers );
			}
			if( state.proficiency && automaticField.proficiency === false ) {
				potentialSpeakers = reduceSpeakers( 3, state.proficiency, potentialSpeakers );
			}

			var listSpeaker = getListSelectorFromQids( 'speaker', dedupe( potentialSpeakers.map( function( x ) { return x[0]; } ) ) );
			var listGender = getListSelectorFromQids( 'gender', dedupe( potentialSpeakers.map( function( x ) { return x[1]; } ) ) );
			var listLanguage = getListSelectorFromQids( 'language', dedupe( potentialSpeakers.map( function( x ) { return x[2]; } ) ) );
			var listProficiency = getListSelectorFromQids( 'proficiency', dedupe( potentialSpeakers.map( function( x ) { return x[3]; } ) ) );

			// Update the selectors with the restricted values
			updateSelector( 'speaker' )( listSpeaker, emptyValue );
			updateSelector( 'gender' )( listGender, emptyValue );
			updateSelector( 'language' )( listLanguage, emptyValue );
			updateSelector( 'proficiency' )( listProficiency, emptyValue );

			request = {
				speaker: getQValue( 'speaker' ),
				gender: getQValue( 'gender' ),
				language: getQValue( 'language' ),
				proficiency: getQValue( 'proficiency' ),
			};
			console.debug( automaticField );
		}

		function initSelectors() {

			$.getJSON(
				'https://lingualibre.org/bigdata/namespace/wdq/sparql',
				{
					query: sparqlGlobal['speaker']
				}
			).done( updateSelector( 'speaker', true ) );

			$.getJSON(
				'https://lingualibre.org/bigdata/namespace/wdq/sparql',
				{
					query: sparqlGlobal['gender']
				}
			).done( updateSelector( 'gender', true ) );

			$.getJSON(
				'https://lingualibre.org/bigdata/namespace/wdq/sparql',
				{
					query: sparqlGlobal['language']
				}
			).done( updateSelector( 'language', true ) );

			$.getJSON(
				'https://lingualibre.org/bigdata/namespace/wdq/sparql',
				{
					query: sparqlGlobal['proficiency']
				}
			).done( updateSelector( 'proficiency', true ) );

			$('#sndlib-savefiltersearch').wrapInner( '<a style="color:white"></a>' );
		}

		function reset() {
			request = {
				speaker: '',
				gender: '',
				language: '',
				proficiency: '',
			};
			automaticField = {
				'speaker': null,
				'gender': null,
				'language': null,
				'proficiency': null,
			};
			updateSelector( 'speaker', true )( list.speaker );
			updateSelector( 'gender', true )( list.gender );
			updateSelector( 'language', true )( list.language );
			updateSelector( 'proficiency', true )( list.proficiency );
			$( '#sndlib-audioresults' ).html( '' );
			$( '#sndlib-audioPages' ).html( '' );
		}

		function reduceSpeakers( index, value, arr ) {
			return arr.filter( function( x ) {
				return x[index] === value;
			} );
		}

		// Add button 'Search'
		$( '#sndlib-gosearch' ).html('').append(
			( new OO.ui.ButtonInputWidget( {
				label: messages['button-gosearch'][userLanguage] ? messages['button-gosearch'][userLanguage] : messages['button-gosearch'].en
			} ) ).on( 'click', factoryDoQuery( 'random' ) ).$element
		);

		$( '#sndlib-gosearch-last-records' ).html('').append(
			( new OO.ui.ButtonInputWidget( {
				label: messages['button-gosearch-last-records'][userLanguage] ? messages['button-gosearch-last-records'][userLanguage] : messages['button-gosearch-last-records'].en
			} ) ).on( 'click', factoryDoQuery( 'last' ) ).$element
		);

		// Add button 'Reset'
		$( '#sndlib-resetsearch' ).html('').append(
			( new OO.ui.ButtonInputWidget( {
				label: messages['button-resetsearch'][userLanguage] ? messages['button-resetsearch'][userLanguage] : messages['button-resetsearch'].en
			} ) ).on( 'click', reset ).$element
		);

		// Add button 'Datasets' - it seems that OOUI ButtonWidget.setHref does not work
		var buttonDatasetsA = $( '<a href="https://lingualibre.org/datasets/"></a>' );
		var buttonDatasets = new OO.ui.ButtonInputWidget( {
				label: messages['button-datasets'][userLanguage] ? messages['button-datasets'][userLanguage] : messages['button-datasets'].en,
			} );
		var buttonDatasetsLinked = buttonDatasetsA.append( buttonDatasets.$element );
		$( '#sndlib-datasetsButton' ).html('').append( buttonDatasetsLinked );

		function setDownloadLink( data ) {
			if ( data.results === undefined || data.results.bindings === undefined ) {
				displayError( 'error: no result from SPARQL' );
				return;
			}

			function escapeCsv( val ) {
				if( !val ) {
					return '""';
				}
				return '"' + val.value.replaceAll( '"', '""' ) + '"';
			}
			
			var csv = 'data:application/csv;charset=utf-8,' + encodeURIComponent( data.results.bindings.map( function( x ) {
					return [
						escapeCsv( x.speakerName ),
						escapeCsv( x.genderLabel ),
						escapeCsv( x.languageLabel ),
						escapeCsv( x.proficiencyLabel ),
						escapeCsv( x.word ),
						escapeCsv( x.file ),
						escapeCsv( x.date ),
					].join( ',' );
				} ).join( "\n" ) );

			$('#sndlib-savefiltersearch a').attr( {
				download: 'export-lili.csv',
				href: csv,
			} );
		}

		// Display results
		function createAudioBoxesForSearch( data ) {
			if ( data.results === undefined || data.results.bindings === undefined ) {
				displayError( 'error: no result from SPARQL' );
				return;
			}
			if ( data.results.bindings.length < 1 ) {
				$( '#sndlib-audioresults' ).html( messages['msg-no-results'][userLanguage] ? messages['msg-no-results'][userLanguage] : messages['msg-no-results'].en );
				$( '#sndlib-audioPages' ).html( '' );
				return;
			}
			var length = data.results.bindings.length <= nbTotalResults ? data.results.bindings.length : nbTotalResults;
			var moreResults = data.results.bindings.length > nbTotalResults;
			console.debug( 'SPARQL results', data.results.bindings );
			function displayAudioBoxes( index ) {
				$( '#sndlib-audioresults' ).html( '' );
				for( var i = 0; i < nbResultsPerPage && i+index*nbResultsPerPage < length; i++ ) {
					var box = $( '<div class="audiobox"> <div class="ab-playbutton"><i></i></div> <div> <div class="ab-title">...</div> <div class="ab-metadata">...</div> </div> </div>' );
					var audiobox = new AudioBox( data.results.bindings[ i+index*nbResultsPerPage ].record.value.substr( 31 ), box );
					$( '#sndlib-audioresults' ).append( box );
				}
				function applyFn( i ) {
					return function() {
						displayAudioBoxes( i );
						return false;
					}
				}
				$( '#sndlib-audioPages' ).html( '' );
				if( length > nbResultsPerPage ) {
					for( var i = 0; i < Math.ceil( length/nbResultsPerPage ); i++ ) {
						var link = $( '<a href="#"' + ( i === index ? ' class="selected"' : '' ) + '>' + (i+1) + '</a>' ).on( 'click', applyFn( i ) );
						$( '#sndlib-audioPages' ).append( link ).append( i < Math.ceil( length/nbResultsPerPage )-1 ? '&nbsp;' : '' );
					}
				}
				if( moreResults ) {
					$( '#sndlib-audioPages' ).append( messages['msg-other-records'][userLanguage] ? messages['msg-other-records'][userLanguage] : messages['msg-other-records'].en );
				}
			}
			displayAudioBoxes( 0 );

			var recordsEntities = data.results.bindings.map( function( x ) {
				return 'entity:' + x.record.value.substr( 31 );
			} ).join( ',' );
			console.debug( recordsEntities );
			console.debug( 'SELECT ?record ?speakerName ?languageLabel ?word ?file ?date ?proficiencyLabel ?genderLabel WHERE { ?record prop:P2 entity:Q2 ; prop:P4 ?language ; prop:P5 ?speaker ; prop:P7 ?word ; prop:P3 ?file ; prop:P6 ?date . ?speaker llp:P4 [ llv:P4 ?language ; llq:P16 ?proficiency ] ; prop:P8 ?gender . ?speaker rdfs:label ?speakerName . FILTER( ?record IN (' + recordsEntities + ') ) . FILTER( LANG( ?speakerName ) = "en" ) . SERVICE wikibase:label { bd:serviceParam wikibase:language "' + userLanguage + ',fr,en" } }' );

			$.getJSON(
				'https://lingualibre.org/bigdata/namespace/wdq/sparql',
				{
					query: 'SELECT ?record ?speakerName ?languageLabel ?word ?file ?date ?proficiencyLabel ?genderLabel WHERE { ?record prop:P2 entity:Q2 ; prop:P4 ?language ; prop:P5 ?speaker ; prop:P7 ?word ; prop:P3 ?file ; prop:P6 ?date . ?speaker llp:P4 [ llv:P4 ?language ; llq:P16 ?proficiency ] ; prop:P8 ?gender . ?speaker rdfs:label ?speakerName . FILTER( ?record IN (' + recordsEntities + ') ) . FILTER( LANG( ?speakerName ) = "en" ) . SERVICE wikibase:label { bd:serviceParam wikibase:language "' + userLanguage + ',fr,en" } }'
				}
			).done( setDownloadLink );

		}

		// Do SPARQL request from filters
		function factoryDoQuery( order ) {

			return function doQuery() {

				request = {
					speaker: getQValue( 'speaker' ),
					gender: getQValue( 'gender' ),
					language: getQValue( 'language' ),
					proficiency: getQValue( 'proficiency' ),
					order: order,
				};

				var potentialSpeakers = speakersCriteria;

				var query = "SELECT ?record ?date WHERE { ?record prop:P2 entity:Q2 ; prop:P4 ?language ; prop:P5 ?speaker ; prop:P6 ?date . ";
				if( request.language ) {
					query += "?record prop:P4 entity:" + request.language + " . ";
					potentialSpeakers = reduceSpeakers( 2, request.language, potentialSpeakers );
				}
				if( request.speaker ) {
					query += "?record prop:P5 entity:" + request.speaker + " . ";
					potentialSpeakers = reduceSpeakers( 0, request.speaker, potentialSpeakers );
				}
				if( request.gender ) {
					potentialSpeakers = reduceSpeakers( 1, request.gender, potentialSpeakers );
				}
				if( request.proficiency ) {
					potentialSpeakers = reduceSpeakers( 3, request.proficiency, potentialSpeakers );
					query += "?speaker llp:P4 [ llv:P4 ?language ; llq:P16 entity:" + request.proficiency + " ] . ";
				}
				if( potentialSpeakers.length < speakersCriteria.length ) {
					var listSpeakers = dedupe( potentialSpeakers.map( function( x ) {
						return 'entity:' + x[0];
					} ) ).join();
					console.debug( speakersCriteria, potentialSpeakers, listSpeakers );
					if( listSpeakers.length < 70 ) {
						query += 'FILTER( ?speaker IN (' + listSpeakers + ') )';
					}
				}
				query += '}';
				if( order === 'random' ) {
					query += ' ORDER BY RAND()';
				} else if( order === 'last' ) {
					query += ' ORDER BY DESC(?date)';
				}
				query += ' LIMIT ' + (nbTotalResults+1);
				if( order === 'random' ) {
					query += '\n# ' + Date.now();
				}

				// Voir https://commons.wikimedia.org/wiki/Category:Throbbers - il faut que le fond soit transparent
				$( '#sndlib-audioresults' ).html( '' ).append( '<img src="https://upload.wikimedia.org/wikipedia/commons/7/7a/Balls.gif" width="50" />' );
				$( '#sndlib-audioPages' ).html( '' );

				// Execute the request
				var result = $.getJSON(
					'https://lingualibre.org/bigdata/namespace/wdq/sparql',
					{
						query: query,
						//Accept: 'application/sparql-results+json'
					}
				);

				result.then( createAudioBoxesForSearch, displayError );
			}
		}

		initSelectors();

	} );

} );

/*Confirmation message when users try to leave the Record Wizard
(works from the second step onwards) */
$( function() {
	if(mw.config.get( 'wgPageName' ) !== 'Special:RecordWizard') {
		return;
	}
	window.onbeforeunload = function() {
  		return "Do you want to leave the page? All your unsaved changes will be lost.";
	};
});