MediaWiki
Difference between revisions of "Common.js"
(dans l’interface des contributions : ajout du bouton 'Rechercher', correction du bug sous Windows) |
(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)) |
||
Line 176: | Line 176: | ||
return; | return; | ||
} | } | ||
− | + | const userLanguage = mw.config.get( 'wgUserLanguage' ); | |
− | + | const messages = { | |
− | 'no-results': { | + | 'msg-no-results': { |
en: 'No results.', | en: 'No results.', | ||
de: 'Keine Ergebnisse.', | de: 'Keine Ergebnisse.', | ||
fr: 'Pas de résultat.', | fr: 'Pas de résultat.', | ||
+ | //qqq: 'Message when there are no results.', | ||
}, | }, | ||
− | 'gosearch': { | + | 'button-gosearch': { |
en: 'Search', | en: 'Search', | ||
de: 'Suchen', | de: 'Suchen', | ||
fr: 'Rechercher', | fr: 'Rechercher', | ||
+ | //qqq: 'Button where the user clicks to search.', | ||
}, | }, | ||
− | 'resetsearch': { | + | 'button-resetsearch': { |
en: 'Reset', | en: 'Reset', | ||
de: 'Löschen', | de: 'Löschen', | ||
fr: 'Effacer', | 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.', | ||
}, | }, | ||
}; | }; | ||
Line 203: | Line 226: | ||
}; | }; | ||
var sparqlGlobal = { | var sparqlGlobal = { | ||
− | 'speaker': 'SELECT | + | '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', | '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', | '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', | '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', | 'speaker': '#filteruser', | ||
'gender': '#filtergender', | 'gender': '#filtergender', | ||
Line 228: | Line 237: | ||
'proficiency': '#filterlevelofproficiency', | 'proficiency': '#filterlevelofproficiency', | ||
}; | }; | ||
− | + | const unknown = {}; | |
+ | |||
+ | request = { | ||
+ | 'speaker': '', | ||
+ | 'gender': '', | ||
+ | 'language': '', | ||
+ | 'proficiency': '', | ||
+ | }; | ||
+ | lastRequest = { | ||
'speaker': '', | 'speaker': '', | ||
'gender': '', | 'gender': '', | ||
Line 234: | Line 251: | ||
'proficiency': '', | 'proficiency': '', | ||
}; | }; | ||
− | + | speakersCriteria = []; | |
+ | list = { | ||
+ | 'speaker': null, | ||
+ | 'gender': null, | ||
+ | 'language': null, | ||
+ | 'proficiency': null, | ||
+ | }; | ||
+ | mapping = { | ||
+ | 'speaker': null, | ||
+ | 'gender': null, | ||
+ | 'language': null, | ||
+ | 'proficiency': null, | ||
+ | }; | ||
− | function | + | // Helper function to obtain the Qvalue of some selector |
− | if( | + | 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 ''; | return ''; | ||
} | } | ||
− | + | } | |
− | if( | + | |
− | 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]; | ||
+ | } ); | ||
} | } | ||
− | function updateSelector( type ) { | + | // 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 ) { | 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; | 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]( { | oouiSelectors[type] = new OO.ui[widgetType]( { | ||
− | placeholder: | + | placeholder: messages['placeholder-'+type][userLanguage] ? messages['placeholder-'+type][userLanguage] : messages['placeholder-'+type].en, |
− | |||
menu: { | menu: { | ||
filterFromInput: true, | filterFromInput: true, | ||
− | items: | + | items: values.map( function( x ) { |
return new OO.ui.MenuOptionWidget( { | return new OO.ui.MenuOptionWidget( { | ||
− | data: x[ type + 'Label' ].value + " (" + x[type].value.substr( 31 ) + ")", | + | data: widgetType === 'ComboBoxInputWidget' ? 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 276: | Line 373: | ||
} | } | ||
} ); | } ); | ||
− | if( | + | if( widgetType === 'DropdownWidget' && values.length > 2 ) { |
− | if( widgetType === 'ComboBoxInputWidget' ) { | + | 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 ) ); | ||
} | } | ||
Line 288: | Line 388: | ||
); | ); | ||
− | + | 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]; } ) ) ) ); | ||
+ | |||
} | } | ||
Line 304: | Line 453: | ||
query: sparqlGlobal['speaker'] | query: sparqlGlobal['speaker'] | ||
} | } | ||
− | ).done( updateSelector( 'speaker' ) ); | + | ).done( updateSelector( 'speaker', true ) ); |
$.getJSON( | $.getJSON( | ||
Line 311: | Line 460: | ||
query: sparqlGlobal['gender'] | query: sparqlGlobal['gender'] | ||
} | } | ||
− | ).done( updateSelector( 'gender' ) ); | + | ).done( updateSelector( 'gender', true ) ); |
$.getJSON( | $.getJSON( | ||
Line 318: | Line 467: | ||
query: sparqlGlobal['language'] | query: sparqlGlobal['language'] | ||
} | } | ||
− | ).done( updateSelector( 'language' ) ); | + | ).done( updateSelector( 'language', true ) ); |
$.getJSON( | $.getJSON( | ||
Line 325: | Line 474: | ||
query: sparqlGlobal['proficiency'] | query: sparqlGlobal['proficiency'] | ||
} | } | ||
− | ).done( updateSelector( 'proficiency' ) ) | + | ).done( updateSelector( 'proficiency', true ) ); |
− | |||
} | } | ||
function reset() { | 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( '' ); | $( '#audioresults' ).html( '' ); | ||
} | } | ||
+ | function reduceSpeakers( index, value, arr ) { | ||
+ | return arr.filter( function( x ) { | ||
+ | return x[index] === value; | ||
+ | } ); | ||
+ | } | ||
+ | |||
+ | // Add button 'Search' | ||
$( '#gosearch' ).html('').append( | $( '#gosearch' ).html('').append( | ||
( new OO.ui.ButtonInputWidget( { | ( new OO.ui.ButtonInputWidget( { | ||
− | label: messages['gosearch'][userLanguage] ? messages['gosearch'][userLanguage] : messages['gosearch'] | + | label: messages['button-gosearch'][userLanguage] ? messages['button-gosearch'][userLanguage] : messages['button-gosearch'].en |
− | } ) ).on( 'click', | + | } ) ).on( 'click', doQuery ).$element |
); | ); | ||
+ | // Add button 'Reset' | ||
$( '#resetsearch' ).html('').append( | $( '#resetsearch' ).html('').append( | ||
( new OO.ui.ButtonInputWidget( { | ( new OO.ui.ButtonInputWidget( { | ||
− | label: messages['resetsearch'][userLanguage] ? messages['resetsearch'][userLanguage] : messages['resetsearch'] | + | label: messages['button-resetsearch'][userLanguage] ? messages['button-resetsearch'][userLanguage] : messages['button-resetsearch'].en |
} ) ).on( 'click', reset ).$element | } ) ).on( 'click', reset ).$element | ||
); | ); | ||
Line 354: | Line 518: | ||
} | } | ||
if ( data.results.bindings.length < 1 ) { | if ( data.results.bindings.length < 1 ) { | ||
− | $( '#audioresults' ).html( messages['no-results'][userLanguage] ? messages['no-results'][userLanguage] : messages['no-results'] | + | $( '#audioresults' ).html( messages['msg-no-results'][userLanguage] ? messages['msg-no-results'][userLanguage] : messages['msg-no-results'].en ); |
return; | return; | ||
} | } | ||
Line 367: | Line 531: | ||
var audiobox = new AudioBox( data.results.bindings[ i+index*10 ].record.value.substr( 31 ), box ); | var audiobox = new AudioBox( data.results.bindings[ i+index*10 ].record.value.substr( 31 ), box ); | ||
$( '#audioresults' ).append( box ); | $( '#audioresults' ).append( box ); | ||
− | |||
− | |||
} | } | ||
function applyFn( i ) { | function applyFn( i ) { | ||
Line 386: | Line 548: | ||
// Do SPARQL request from filters | // Do SPARQL request from filters | ||
− | function doQuery( | + | function doQuery() { |
− | + | request = { | |
− | if( | + | speaker: getQValue( 'speaker' ), |
+ | gender: getQValue( 'gender' ), | ||
+ | language: getQValue( 'language' ), | ||
+ | proficiency: getQValue( 'proficiency' ), | ||
+ | }; | ||
+ | if( JSON.stringify( request ) === JSON.stringify( lastRequest ) ) { | ||
return; | return; | ||
} | } | ||
− | + | lastRequest = request; | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | console.log( request ); | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | console.log( | ||
// When all fields are empty, do not query | // When all fields are empty, do not query | ||
− | if( !speaker && !gender && !language && !proficiency ) { | + | if( !request.speaker && !request.gender && !request.language && !request.proficiency ) { |
return; | return; | ||
} | } | ||
− | var query = "SELECT | + | var potentialSpeakers = speakersCriteria; |
− | if( | + | |
− | query += "?record prop: | + | 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( | + | if( request.speaker ) { |
− | query += "?record prop:P5 | + | query += "?record prop:P5 entity:" + request.speaker + " . "; |
− | + | potentialSpeakers = reduceSpeakers( 0, request.speaker, potentialSpeakers ); | |
} | } | ||
− | if( | + | if( request.gender ) { |
− | + | potentialSpeakers = reduceSpeakers( 1, request.gender, potentialSpeakers ); | |
− | |||
} | } | ||
− | if( proficiency ) { | + | if( request.proficiency ) { |
− | query += "?speaker llp:P4 [ llv:P4 ?language ; llq:P16 " + proficiency + " ] . " | + | potentialSpeakers = reduceSpeakers( 3, request.proficiency, potentialSpeakers ); |
− | + | query += "?speaker llp:P4 [ llv:P4 ?language ; llq:P16 entity:" + request.proficiency + " ] . "; | |
} | } | ||
− | query += | + | 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 | // Execute the request | ||
Line 472: | Line 610: | ||
); | ); | ||
− | + | result.then( createAudioBoxesForSearch, displayError ); | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
} | } | ||
Revision as of 11:47, 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( 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();
} );
} );