MediaWiki

Difference between revisions of "Common.js"

(nouvelle version pour l’interface de recherche de contributions, beaucoup plus frugale (moins d’appels à Blazegraph) et plus rapide, il y a aussi un essai d’UX à réduire les listes aux choix possibles, par ex quand on choisit un locuteur ça sélectionne automatiquement son genre et les langues qu’iel parle (ce fonctionnement entraîne certains effets de bord étranges quand on délélectionne, ce comportement reste du POC))
m (correction d’un petit bug lorsqu’une valeur est inconnue)
Line 350: Line 350:
  
 
if( !force && getQValue( type ) ) {
 
if( !force && getQValue( type ) ) {
 +
return;
 +
}
 +
 +
// If there are at least one unknown value, do not change the selector
 +
if( values.filter( function( x ) { return x === undefined; } ).length ) {
 
return;
 
return;
 
}
 
}

Revision as of 17:36, 13 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, '_' ) );
}

// Development in final phase of the interface for displaying contributions
$( function (){
	if( mw.config.get( 'wgPageName' ) !== 'User:Nicolas_NALLET' || mw.config.get( 'wgUserName' ) === 'Seb35' ) {
		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 = {};

		request = {
			'speaker': '',
			'gender': '',
			'language': '',
			'proficiency': '',
		};
		lastRequest = {
			'speaker': '',
			'gender': '',
			'language': '',
			'proficiency': '',
		};
		speakersCriteria = [];
		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 = 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];
			} );
			console.log( 'getListSelectorFromQids', type, qids, indexes, r );
			return r;
		}

		// Initialise a selector and keep the list in memory the result for later reuse
		function updateSelector( type, force ) {
			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;
					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.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 ),
							];
						} );
					}
				}

				if( !force && getQValue( type ) ) {
					return;
				}

				// If there are at least one unknown value, do not change the selector
				if( values.filter( function( x ) { return x === undefined; } ).length ) {
					return;
				}

				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-'+type][userLanguage] ? messages['placeholder-'+type][userLanguage] : messages['placeholder-'+type].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 ) {
					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 ) {
					oouiSelectors[type].getMenu().selectItemByData( values[1][type].value.substr( 31 ) );
				}

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

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

			};
		}

		function onChange() {

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

			var potentialSpeakers = speakersCriteria;

			if( request.language ) {
				potentialSpeakers = reduceSpeakers( 2, request.language, potentialSpeakers );
			}
			if( 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 );
			}

			console.log( potentialSpeakers );

			// Update the selectors with the restricted values
			updateSelector( 'speaker' )( getListSelectorFromQids( 'speaker', dedupe( potentialSpeakers.map( function( x ) { return x[0]; } ) ) ) );
			updateSelector( 'gender' )( getListSelectorFromQids( 'gender', dedupe( potentialSpeakers.map( function( x ) { return x[1]; } ) ) ) );
			updateSelector( 'language' )( getListSelectorFromQids( 'language', dedupe( potentialSpeakers.map( function( x ) { return x[2]; } ) ) ) );
			updateSelector( 'proficiency' )( getListSelectorFromQids( 'proficiency', dedupe( potentialSpeakers.map( function( x ) { return x[3]; } ) ) ) );

		}

		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 ) );
		}

		function reset() {
			lastRequest = {
				speaker: '',
				gender: '',
				language: '',
				proficiency: '',
			};
			updateSelector( 'speaker', true )( list.speaker );
			updateSelector( 'gender', true )( list.gender );
			updateSelector( 'language', true )( list.language );
			updateSelector( 'proficiency', true )( 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'
				}
			);

			result.then( createAudioBoxesForSearch, displayError );
		}

		initSelectors();

	} );

} );