User

Difference between revisions of "Seb35/bluell.js"

< User:Seb35

(dans les dropdown, ajout d’un premier élément vide pour désélectionner le choix de la dropdown (on pourrait mettre un texte comme « (effacer) » plutôt qu’un champ vide))
(ajout d’un premier élément vide également pour les ComboBoxInputWidget afin que l’utilisateur ait un moyen de désélectionner facilement (même si sur ce type de widget c’est déjà possible avec Ctrl+A Suppr, mais pas forcément intuitif))
Line 143: Line 143:
 
// We dedupe because the 'speaker' (only) has duplicate (ordered) values
 
// We dedupe because the 'speaker' (only) has duplicate (ordered) values
 
const v = values.results.bindings;
 
const v = values.results.bindings;
list[type] = dedupe( v, function( x ) { return x[type].value; } );
+
var emptyItem = [ {} ];
if( widgetType === 'DropdownWidget' ) {
+
emptyItem[0][type] = { value: '' };
var emptyItem = [ {} ];
+
emptyItem[0][type+'Label'] = { value: ' ' };
emptyItem[0][type] = { value: '' };
+
list[type] = emptyItem.concat( dedupe( v, function( x ) { return x[type].value; } ) );
emptyItem[0][type+'Label'] = { value: ' ' };
 
list[type] = emptyItem.concat( list[type] );
 
}
 
  
 
// Save the mapping Qxx → index to update the list from a list of Qxx (see doQuery)
 
// Save the mapping Qxx → index to update the list from a list of Qxx (see doQuery)
Line 176: Line 173:
 
menu: {
 
menu: {
 
filterFromInput: true,
 
filterFromInput: true,
items: list[type].map( function( x ) {
+
items: list[type].map( function( x, i ) {
 
return new OO.ui.MenuOptionWidget( {
 
return new OO.ui.MenuOptionWidget( {
data: widgetType === 'ComboBoxInputWidget' ? x[ type + 'Label' ].value + " (" + x[type].value.substr( 31 ) + ")" : x[type].value.substr( 31 ),
+
data: widgetType === 'ComboBoxInputWidget' && i ? x[ type + 'Label' ].value + " (" + x[type].value.substr( 31 ) + ")" : x[type].value.substr( 31 ),
 
label: x[ type + 'Label' ].value
 
label: x[ type + 'Label' ].value
 
} );
 
} );
Line 184: Line 181:
 
}
 
}
 
} );
 
} );
if( widgetType === 'DropdownWidget' ) {
+
oouiSelectors[type].getMenu().items[0].toggle( false );
oouiSelectors[type].getMenu().items[0].toggle( false );
 
}
 
  
 
if( values.length === 1 ) {
 
if( values.length === 1 ) {

Revision as of 11:16, 11 September 2021

// Development in final phase of the interface for displaying contributions
$( function (){
	if( mw.config.get( 'wgPageName' ) !== 'User:Nicolas_NALLET' ) {
		return;
	}
	const userLanguage = mw.config.get( 'wgUserLanguage' );
	const messages = {
		'msg-no-results': {
			en: 'No results.',
			de: 'Keine Ergebnisse.',
			fr: 'Pas de résultat.',
			//qqq: 'Message when there are no results.',
		},
		'button-gosearch': {
			en: 'Search',
			de: 'Suchen',
			fr: 'Rechercher',
			//qqq: 'Button where the user clicks to search.',
		},
		'button-resetsearch': {
			en: 'Reset',
			de: 'Löschen',
			fr: 'Effacer',
			//qqq: 'Button where the user clicks to reset the form.',
		},
		'placeholder-speaker': {
			en: '👤 Speaker',
			fr: '👤 Locuteur',
			//qqq: 'Placeholder for the field Speaker.',
		},
		'placeholder-gender': {
			en: '♀️ ♂️ Speaker\'s gender',
			fr: '♀️ ♂️ Genre du locuteur',
			//qqq: 'Placeholder for the field Gender.',
		},
		'placeholder-language': {
			en: '🏳️ Language',
			fr: '🏳️ Langue',
			//qqq: 'Placeholder for the field Language.',
		},
		'placeholder-proficiency': {
			en: '🥇 Level of proficiency',
			fr: '🥇 Niveau de compétence',
			//qqq: 'Placeholder for the field Proficiency.',
		},
	};
	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': '#filteruser',
			'gender': '#filtergender',
			'language': '#filterlanguage',
			'proficiency': '#filterlevelofproficiency',
		};
		const unknown = {};

		var request = {
			'speaker': '',
			'gender': '',
			'language': '',
			'proficiency': '',
		}, lastRequest = {
			'speaker': '',
			'gender': '',
			'language': '',
			'proficiency': '',
		}, speakersCriteria = [];
		const list = {
			'speaker': null,
			'gender': null,
			'language': null,
			'proficiency': null,
		}, mapping = {
			'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 '';
			} 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 = qids.map( function( x ) {
				return mapping[type][x];
			} ).sort();
			return indexes.map( function( x ) {
				return list[type][x];
			} );
		}

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

				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;
					var emptyItem = [ {} ];
					emptyItem[0][type] = { value: '' };
					emptyItem[0][type+'Label'] = { value: ' ' };
					list[type] = emptyItem.concat( dedupe( v, function( x ) { return x[type].value; } ) );

					// Save the mapping Qxx → index to update the list from a list of Qxx (see doQuery)
					console.log( 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 ),
							];
						} );
					}
				}

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

				if( values.length === 1 ) {
					if( widgetType === 'ComboBoxInputWidget' ) {
						oouiSelectors[type].setValue( v[0][ type + 'Label' ].value + " (" + v[0][type].value.substr( 31 ) + ")" );
					} else {
						oouiSelectors[type].getMenu().selectItemByData( v[0][ type + 'Label' ].value + " (" + v[0][type].value.substr( 31 ) + ")" );
					}
				}

				$( htmlElements[type] ).html('').append(
					oouiSelectors[type].$element
				);

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

			};
		}

		function initSelectors() {

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

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

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

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

		function reset() {
			lastRequest = {
				speaker: '',
				gender: '',
				language: '',
				proficiency: '',
			};
			updateSelector( 'speaker' )( list.speaker );
			updateSelector( 'gender' )( list.gender );
			updateSelector( 'language' )( list.language );
			updateSelector( 'proficiency' )( list.proficiency );
			$( '#audioresults' ).html( '' );
		}

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

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

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

		// 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 ) {
				$( '#audioresults' ).html( messages['msg-no-results'][userLanguage] ? messages['msg-no-results'][userLanguage] : messages['msg-no-results'].en );
				return;
			}
			var length = data.results.bindings.length;
			console.log( data.results.bindings );
			function displayAudioBoxes( index ) {
				$( '#audioresults' ).html( '' );
				console.log( 'index', index );
				for( var i = 0; i < 10 && i+index*10 < length; i++ ) {
					console.log( 'i effectif', i+index*10 );
					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*10 ].record.value.substr( 31 ), box );
					$( '#audioresults' ).append( box );
				}
				function applyFn( i ) {
					return function() {
						console.log( i );
						displayAudioBoxes( i );
						return false;
					}
				}
				for( var i = 0; i < Math.ceil( length/10 ); i++ ) {
					var link = $( '<a href="#">' + (i+1) + '</a>' ).on( 'click', applyFn( i ) );
					$( '#audioresults' ).append( link ).append( ' ' );
				}
			}
			displayAudioBoxes( 0 );
		}

		// Do SPARQL request from filters
		function doQuery() {

			request = {
				speaker: getQValue( 'speaker' ),
				gender: getQValue( 'gender' ),
				language: getQValue( 'language' ),
				proficiency: getQValue( 'proficiency' ),
			};
			if( JSON.stringify( request ) === JSON.stringify( lastRequest ) ) {
				return;
			}

			lastRequest = request;

			console.log( request );

			// When all fields are empty, do not query
			if( !request.speaker && !request.gender && !request.language && !request.proficiency ) {
				return;
			}

			var potentialSpeakers = speakersCriteria;

			var query = "SELECT ?record WHERE { ?record prop:P2 entity:Q2 ; prop:P4 ?language ; prop:P5 ?speaker . ";
			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();
				query += 'FILTER( ?speaker IN (' + listSpeakers + ') )';
			}
			query += "} LIMIT 100";

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

			console.log( potentialSpeakers );
			console.log( listSpeakers );

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

			// Update the selectors with the restricted values
			// This could have been done a bit earlier, but we launched the SPARQL request before
			if( request.speaker ) {
				updateSelector( 'speaker' )( getListSelectorFromQids( 'speaker', potentialSpeakers.map( function( x ) { return x[0]; } ) ) );
			}
			if( request.gender ) {
				updateSelector( 'gender' )( getListSelectorFromQids( 'gender', potentialSpeakers.map( function( x ) { return x[1]; } ) ) );
			}
			if( request.language ) {
				updateSelector( 'language' )( getListSelectorFromQids( 'language', potentialSpeakers.map( function( x ) { return x[2]; } ) ) );
			}
			if( request.proficiency ) {
				updateSelector( 'proficiency' )( getListSelectorFromQids( 'proficiency', potentialSpeakers.map( function( x ) { return x[3]; } ) ) );
			}

			result.then( createAudioBoxesForSearch, displayError );
		}

		initSelectors();

	} );

} );