Utente:Seb35/common.js

Da Wikipedia, l'enciclopedia libera.
Vai alla navigazione Vai alla ricerca

Questa pagina definisce alcuni parametri di aspetto e comportamento generale di tutte le pagine. Per personalizzarli vedi Aiuto:Stile utente.


Nota: dopo aver salvato è necessario pulire la cache del proprio browser per vedere i cambiamenti (per le pagine globali è comunque necessario attendere qualche minuto). Per Mozilla / Firefox / Safari: fare clic su Ricarica tenendo premuto il tasto delle maiuscole, oppure premere Ctrl-F5 o Ctrl-R (Command-R su Mac); per Chrome: premere Ctrl-Shift-R (Command-Shift-R su un Mac); per Konqueror: premere il pulsante Ricarica o il tasto F5; per Opera può essere necessario svuotare completamente la cache dal menù Strumenti → Preferenze; per Internet Explorer: mantenere premuto il tasto Ctrl mentre si preme il pulsante Aggiorna o premere Ctrl-F5.

$( function () {

	// High level overview:
	// 1. The template [[Template:Category filters]] specifies the category filters
	// 2. The basic metadata of the Wikidata properties corresponding to the filters are retrieved: type, min/max/regex constraints, authorised values if applicable
	// 3. Widgets are created for each filter (dropdown lists, number input boxes…) with constraints if applicable
	// 4. Depending on the number of articles in the category (<= 200 is a small category), the search strategy is 'prefetch' or 'online':
	//    A. Prefetch strategy:
	//       1. A SPARQL request is done when the page loads, requesting these Wikidata properties thanks to SERVICE wikibase:mwapi
	//       2. When the user enters the reque st, the filtering is done locally by the script by reading the SPARQL result
	//       3. The logic is non-binary:
	//            * true = the Wikipedia article/Wikidata item matches all the filters,
	//            * false = it does not match at least one filter,
	//            * noProperty = one requested property is not present for the Wikidata item, so we are unsure if it matches the filters
	//    B. Online strategy:
	//       The SPARQL request is executed online with the specified filters

	if( mw.config.get( 'wgNamespaceNumber' ) !== 14 || $( '#category-filters' ).length === 0 ) {
		return;
	}

	categoryFiltersTranslations = {

		'en': {
			'title-search-filters': 'Search filters',
			'placeholder-WikibaseItem': 'Select one or more values',
			'type-Time-after': 'Between…',
			'type-Time-before': 'and…',
			'type-Quantity-after': 'Between…',
			'type-Quantity-before': 'and…',
			'button-search': 'Search',
			'button-reset': 'Reset filters',
			'switch-partial-results': 'Show also entries with undetermined values',
			'total-pages-displayed': '($1 displayed after filtering)',
			'tooltip-missing-wikidata': 'This article has no link to Wikidata.',
			'tooltip-missing-property': 'This article misses at least one property amongst the filters.',
			'no-results': 'No results',
		},

		'it': {
			'title-search-filters': 'Filtri di ricerca',
			'placeholder-WikibaseItem': 'Seleziona uno o più valori',
			'type-Time-after': 'Tra…',
			'type-Time-before': 'e…',
			'type-Quantity-after': 'Tra…',
			'type-Quantity-before': 'e…',
			'button-search': 'Cerca',
			'button-reset': 'Azzerare i filtri',
			'switch-partial-results': 'Mostra anche voci con valori non determinati',
			'total-pages-displayed': '($1 visualizzato dopo il filtraggio)',
			'tooltip-missing-wikidata': 'Questo articolo non ha un collegamento a Wikidata.',
			'tooltip-missing-property': 'In questo articolo manca almeno una proprietà tra i filtri.',
			'no-results': 'Nessun risultato',
		},

		'fr': {
			'title-search-filters': 'Filtres de recherche',
			'placeholder-WikibaseItem': 'Sélectionnez une ou plusieurs valeurs',
			'type-Time-after': 'Entre…',
			'type-Time-before': 'et…',
			'type-Quantity-after': 'Entre…',
			'type-Quantity-before': 'et…',
			'button-search': 'Rechercher',
			'button-reset': 'Réinitialiser les filtres',
			'switch-partial-results': 'Afficher également les entrées avec des résultats indéterminés',
			'total-pages-displayed': '($1 affichées après filtrage)',
			'tooltip-missing-wikidata': 'Cet article n’est pas lié à Wikidata.',
			'tooltip-missing-property': 'Il manque à cet article au moins une propriété parmi les filtres.',
			'no-results': 'Aucun résultat',
		}
	};

	// General regexes
	// These can be used as a very filter filter to check user-entered values
	var generalRegexes = {
		'wikibase-item': 'Q[0-9]+',
		'time-year': '-?[0-9]+',
		'time-day': '-?[0-9]+-(?:0[1-9]|1[012])-(?:0[1-9]|[12][0-9]|3[01])',
		'quantity': '-?[0-9]+(?:[,.][0-9]+)?',
		'quantity-integer': '-?[0-9]+',
	};
	
	// These are property metadata to be able to create selectors with a given quality level
	// This object is populated a few lines below
	properties = { // TODO add "var" keyword for production
		/** EXAMPLES OF PROPERTIES TYPES - this is now loaded from Wikidata, but the schema (name of each characteristics and explanation) should be documented somewhere (possibly remain here)
		'P21': {
			// undefined|"WikibaseItem"|"Quantity"|"Time"|"Monolingualtext"|"String"|"Url"|"ExternalId" – type of the property
			type: 'WikibaseItem',
			// undefined|string|null – label of the property in the user language; null means we tried to get the name and we failed
			label: undefined,
			// 0|1|2|3|4 – quality level where:
			//   - 0: we don’t know anything about the property, not even its type. Nicolas: 2022-03-21 still exists ? Not present on https://it.wikipedia.org/wiki/Discussioni_progetto:Coordinamento/Categorie/Progetto_filtro_categorie - Seb: 2022-03-21: this is the initialisation value, before any request to Wikidata to know the type, see beginning of getPropertiesMetadata()
			//   - 1: we know the type of the property
			//   - 2: if applicable, we know a regex of acceptable values (for string-typed properties) or a minimum and/or maximum of acceptable values (for quantity-typed properties)
			//   - 3: we know all values independently of the specific category we are visiting
			//   - 4: we know all values present in the specific category we are visiting
			level: 1,
			// undefined|Number – minimum value accepted for this property
			min: undefined,
			// undefined|Number – maximum value accepted for this property
			max: undefined,
			// undefined|string – regex constraint for this property
			regex: undefined,
			// undefined|null|string[]|Object[] – all values of the property, either globally either for the specific category if possible
			// null means we tried to get the type and we failed
			// for most property types this is string[], but for wikibase-item this is Object[] (each object is { item: (string), lang: (string), label: (string) }
			values: undefined,
		},
		'P569': { // date of birth 
			type: 'Time',
			label: undefined,
			level: 1,
		},
		'P19': { // place of birth
			type: 'WikibaseItem',
			label: undefined,
			level: 1,
		},
		'P364': { // original language of film or TV show
			type: 'WikibaseItem',
			label: undefined,
			level: 1,
		},
		'P495': { // country of origin
			type: 'WikibaseItem',
			label: undefined,
			level: 1,
		},
		'P577': { // publication date
			type: 'Time',
			label: undefined,
			level: 1,
		},
		/* END OF EXAMPLES OF PROPERTIES TYPES */
	};

	categoryFilters = {
		'specFilters': $( '#category-filters-list' ).text().trim().replace( / /g, '' ),
		'listFilters': $( '#category-filters-list' ).text().trim().replace( / /g, '' ).split( ',' ),
		'sparqlQuery': null,
		'searchStrategy': undefined,
		'oouiWidgetsFilters': {},
		'valuesFromWikidata': null,
		'valuesFromWikidataLight': null,
		'sitelinks': {},
		'articlesToWikidata': null,
		'allSubjects': null,
		'cachePV': {},
	};

	var noQid = {}, // [unused] Marker for a Wikipedia article without corresponding Wikidata item
		noProperty = {}; // Marker for a Wikipedia article with a corresponding Wikidata item, but one requested property is missing => the result of the request is uncertain

	var	userLanguage = mw.config.get( 'wgUserLanguage' ),
		defaultLanguage = categoryFiltersTranslations[userLanguage] ? userLanguage : 'it',
		defaultMessages = categoryFiltersTranslations[defaultLanguage],
		itMessages = categoryFiltersTranslations.it,
		sparqlLanguages = defaultLanguage === 'it' ? 'it,en' : defaultLanguage + ',it,en';
	function msg( msgid ) {
		return defaultMessages[msgid] ? defaultMessages[msgid] : ( itMessages[msgid] ? itMessages[msgid] : '⧼' + msgid + '⧽' );
	}

	if( !categoryFilters.specFilters.match( /^P[0-9]+(,P[0-9]+)*$/ ) ) {
		console.error( 'Wrong parameter “filter”: ' + categoryFilters.specFilters );
		return;
	}

	events = new OO.EventEmitter();
	var initialised = false;
	var widgetSpinner;

	// Some heuristics to evaluate the strategy depending on the category size
    var isBigCategory = $( '#mw-pages > a' ).length > 0; // If there is such link, it is "The following 200 pages are in this category, out of N total."
	// TODO some languages don’t use figures [0-9] like Farsi, Sanskrit, Oriya,… (16 languages according to my search in MediaWiki languages files)
	var numberOfArticles = $( '#mw-pages > p' ).text().replace( /\b200\b/, '' ).match( /[0-9]+(?:[.,  ][0-9]+)*/ );
    if( Array.isArray( numberOfArticles ) ) {
		numberOfArticles = Number( numberOfArticles[0].replace( /[.,  ]/g, '' ) );
		numberOfArticles = isNaN( numberOfArticles ) ? null : numberOfArticles;
	}
	console.debug( 'Big category? ' + ( isBigCategory ? 'yes' : 'false' ) + ', with ' + ( numberOfArticles !== null ? numberOfArticles : 'an unknown number of' ) + ' articles' );
	if( isBigCategory ) {
		categoryFilters.searchStrategy = 'online';
	} else {
		categoryFilters.searchStrategy = 'prefetch';
	}

	// When this promise is resolved, all properties metadata are obtained
	var promiseProperties = getPropertiesMetadata( categoryFilters.listFilters, sparqlLanguages );

	var titleForSparql = mw.config.get( 'wgPageName' ).replaceAll( /\\/g, '\\\\' ).replaceAll( /"/g, '\\"' ); // escape backslashes and double-quote to prevent SPARQL injections
	var filtersSparqlFields = categoryFilters.listFilters.map( function( x ) { return '?' + x + ' ?' + x + 'Label'; } ).join( ' ' );
	var filtersSparqlWhere = categoryFilters.listFilters.map( function( x ) { return '  OPTIONAL { ?item wdt:' + x + ' ?' + x + ' } .'; } ).join( '\n' );
	categoryFilters.sparqlQuery = 'SELECT ?item ?itemLabel ?sitelink ' + filtersSparqlFields + '\nWHERE {\n  SERVICE wikibase:mwapi {\n    bd:serviceParam wikibase:endpoint "it.wikipedia.org"; wikibase:api "Generator"; mwapi:generator "categorymembers"; mwapi:gcmtitle "' + titleForSparql + '"; mwapi:gcmtype "page"; mwapi:inprop "url"; .\n    ?item wikibase:apiOutputItem mwapi:item .\n    ?sitelink wikibase:apiOutput "@fullurl" .\n  }\n  FILTER BOUND (?item) .\n' + filtersSparqlWhere + '\n  # OPTIONAL ONLINE QUERY\n  SERVICE wikibase:label { bd:serviceParam wikibase:language "' + sparqlLanguages + '" }\n}';
	console.debug( categoryFilters.sparqlQuery.replace( /\n  # OPTIONAL ONLINE QUERY/, '' ) );

	if( categoryFilters.searchStrategy === 'prefetch' ) {
		$.getJSON( 'https://query.wikidata.org/bigdata/namespace/wdq/sparql', { query: categoryFilters.sparqlQuery.replace( /\n  # OPTIONAL ONLINE QUERY/, '' ) } ).then( function( x ) {
			console.log( 'Wikidata data received' ); // DEBUG
			console.debug( x ); // DEBUG
			categoryFilters.valuesFromWikidata = x;
			categoryFilters.articlesToWikidata = {};
			x.results.bindings.map( function( y ) { categoryFilters.articlesToWikidata[y.sitelink.value.substr(24)] = y.item.value.substr(31); } );
			addQidToCategoryPages();
			categoryFilters.valuesFromWikidataLight = x.results.bindings.map( function( y ) {
				return Object.entries( y ).reduce(
					function( acc, item ) {
						if( item[1].type === 'uri' && item[1].value.substr( 0, 31 ) === 'http://www.wikidata.org/entity/' ) {
							acc[item[0]] = item[1].value.substr( 31 );
						} else {
							acc[item[0]] = item[1].value;
						}
						return acc;
					}, {} );
			} );
			console.debug( categoryFilters.valuesFromWikidataLight ); // DEBUG
			categoryFilters.sitelinks = categoryFilters.valuesFromWikidataLight.reduce( function( acc, y ) { acc[y.item] = y.sitelink; return acc; }, {} );
			categoryFilters.allSubjects = x.results.bindings.map( function( y ) { return y.item.value.substr( 31 ); } );
			// Update the values of WikibaseItem-type properties with real-used values (level-4 properties)
			function registerValues() {
				categoryFilters.listFilters.map( function( prop ) {
					if( !properties[prop] || properties[prop].type !== 'WikibaseItem' || !categoryFilters.oouiWidgetsFilters[prop] ) {
						return;
					}
					var values = x.results.bindings.filter( function( y ) { return !!y[prop] && y[prop].type === 'uri'; } ).map( function( y ) { return { 'data': y[prop].value.substr( 31 ), 'label': y[prop+'Label'].value }; } );
					categoryFilters.oouiWidgetsFilters[prop][3][0].menu.clearItems();
					categoryFilters.oouiWidgetsFilters[prop][3][0].addOptions( values );
				} );
				$( '#category-filter-spinner' ).addClass( 'category-filter-hidden' );
			}
			if( initialised ) {
				registerValues();
			} else {
				events.once( 'initialised', registerValues );
			}
		} );
	}
	
	mw.util.addCSS( '#category-filters { display: inline-block; border: 1px solid black; padding: 1em; margin-left: 4em; } #category-filters div.filter { margin-bottom: 1em; } #category-filters div.filter-type-time .oo-ui-numberInputWidget-field { width: 12em; } #category-filters .category-filter-hidden, #mw-pages .hidden-by-filter { display: none; } #mw-pages .category-filters-unknown-qid.hidden-by-default, #mw-pages .category-filters-unknown-property.hidden-by-default { display: none; } #category-filter-advanced { margin-top: 1em; } #mw-pages div.category-filter-wrap { display: none; }' );

	mw.loader.load( '/w/index.php?title=Utente:Ypermat/vector.css&action=raw&ctype=text/css', 'text/css' );

	mw.loader.using( ['oojs', 'oojs-ui'], function () {
		promiseProperties.then( loadUI );
	} );

	function loadUI() {

		console.log( properties ); // DEBUG

		widgetSpinner = new OO.ui.ProgressBarWidget( { progress: 0 } );

		// Similar to MenuTagMultiselectWidget but with autocomplete
		MenuTagMultiselectWithAutocompleteWidget = function MenuTagMultiselectWithAutocompleteWidget( config ) {
			OO.ui.MenuTagMultiselectWidget.call( this, config );
		};
		OO.inheritClass( MenuTagMultiselectWithAutocompleteWidget, OO.ui.MenuTagMultiselectWidget );

		/**
		 * @inheritdoc
		 */
		MenuTagMultiselectWithAutocompleteWidget.prototype.onInputChange = function () {
			var value = this.input.getValue(), that = this;
			if( !value ) {
				this.menu.toggle( false );
				this.menu.clearItems();
				return;
			}
			$.getJSON( { url: '/w/rest.php/v1/search/title?q=' + value + '&limit=10', timeout: 5000 } ).then( function( result ) {
				if( result && result.pages ) {
					that.menu.toggle( false );
					that.menu.clearItems();
					var options = result.pages.map( function( x ) { return x.title; } ).map( function( x ) { return { data: x, label: x }; } );
					that.addOptions( options );
					that.menu.toggle( true );
				}
			} );
		};

		var oouiWidgetsFiltersList = categoryFilters.listFilters.map( function( filter ) {

			if( !( filter in properties ) ) {

				throw new Error( 'The property ' + filter + ' should be known at this time in the variable "properties".' );

			} else if( properties[filter].type === 'WikibaseItem' ) {

				var options = [], allowArbitrary = true, multiselectWidget;
				if( properties[filter].level >= 3 && properties[filter].values && properties[filter].values.length ) {
					options = properties[filter].values.filter( function( x ) { return !!x; } ).map( function( x ) { return { 'data': x.item, 'label': x.label }; } );
					allowArbitrary = false;
				}

				// Type WikibaseItem: a list of the items (multiple choices allowed)
				if( allowArbitrary ) {
					multiselectWidget = new MenuTagMultiselectWithAutocompleteWidget( {
						placeholder: msg( 'placeholder-WikibaseItem' ),
						allowArbitrary: true,
						verticalPosition: 'below',
						selected: []
					} );
					multiselectWidget.on( 'change', function( items ) {
						items.forEach( function( item ) {
							if( typeof item.getData() === 'string' && !item.getData().match( /^Q[0-9]+$/ ) ) {
								var api = new mw.Api();
								api.get( {
									action: 'query',
									prop: 'pageprops',
									titles: item.getData(),
								} ).done( function( data ) {
									if( Object.keys( data.query.pages ).length === 1 ) {
										var pageid = Object.keys( data.query.pages )[0];
										if( data.query.pages[pageid].missing !== '' && data.query.pages[pageid].pageprops && data.query.pages[pageid].pageprops.wikibase_item ) {
											item.setData( data.query.pages[pageid].pageprops.wikibase_item );
										} else {
											item.setData( false );
										}
									}
								} );
							}
						} );
					} );
				} else {
					multiselectWidget = new OO.ui.MenuTagMultiselectWidget( {
						placeholder: msg( 'placeholder-WikibaseItem' ),
						allowArbitrary: allowArbitrary,
						verticalPosition: 'below',
						selected: [],
						options: options
					} );
				}

				return [ filter, 'wikibase-item', [ ucFirst( properties[filter].label ) ], [ multiselectWidget ] ];

			} else if( properties[filter].type === 'Time' ) {

				var widgetProperties1 = {}, widgetProperties2 = {};
				if( properties[filter].min && properties[filter].min.match( /^-?[0-9]+-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/ ) ) {
					widgetProperties1.min = Number( properties[filter].min.replace( /^(-?[0-9]+)-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/, '$1' ) );
					widgetProperties2.min = Number( properties[filter].min.replace( /^(-?[0-9]+)-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/, '$1' ) );
					widgetProperties1.placeholder = 'min : ' + properties[filter].min.replace( /^(-?[0-9]+)-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/, '$1' );
				} else {
					widgetProperties1.placeholder = 'min : -∞ (-infinite)';
				}
				if( properties[filter].max && properties[filter].max.match( /^-?[0-9]+-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/ ) ) {
					widgetProperties1.max = Number( properties[filter].max.replace( /^(-?[0-9]+)-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/, '$1' ) );
					widgetProperties2.max = Number( properties[filter].max.replace( /^(-?[0-9]+)-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/, '$1' ) );
					widgetProperties2.placeholder = 'max : ' + properties[filter].max.replace( /^(-?[0-9]+)-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/, '$1' );
				} else {
					widgetProperties2.placeholder = 'max : ∞ (infinite)';
				}

				// Type Time: two number selectors for year-min and year-max
				var yearMinWidget = new OO.ui.NumberInputWidget( widgetProperties1 );
				var yearMaxWidget = new OO.ui.NumberInputWidget( widgetProperties2 );

				return [ filter, 'time', [ ucFirst( properties[filter].label ), msg( 'type-Time-after' ), msg( 'type-Time-before' ) ], [ yearMinWidget, yearMaxWidget ] ];

			 } else if( properties[filter].type === 'Quantity' ) {

			 	var numberWidgetProperties = {};
				if( properties[filter].min && properties[filter].min.match( /^-?[0-9]+\.[0-9]+$/ ) ) {
					numberWidgetProperties.min = Number( properties[filter].min );
				}
				if( properties[filter].max && properties[filter].max.match( /^-?[0-9]+\.[0-9]+$/ ) ) {
					numberWidgetProperties.max = Number( properties[filter].max );
				}
				var numberMinWidget = new OO.ui.NumberInputWidget( numberWidgetProperties );
				var numberMaxWidget = new OO.ui.NumberInputWidget( numberWidgetProperties );
				return [ filter, 'quantity', [ ucFirst( properties[filter].label ), msg( 'type-Quantity-after' ), msg( 'type-Quantity-before' ) ], [ numberMinWidget, numberMaxWidget ] ];

			 } else if( properties[filter].type === 'String' || properties[filter].type === 'Monolingualtext' || properties[filter].type === 'Url' || properties[filter].type === 'ExternalId' ) {

				var stringWidgetProperties = {};
				if( properties[filter].regex ) {
					try {
						stringWidgetProperties.validate = new RegExp( properties[filter].regex );
					} catch( e ) {
						stringWidgetProperties.validate = null;
					}
				}
			 	var stringWidget = new OO.ui.TextInputWidget( stringWidgetProperties );

			 	return [ filter, 'string', [ ucFirst( properties[filter].label ) ], [ stringWidget ] ];

			 }
			// TODO add more widgets here for more types (and possibly subtypes if we want multiple widgets for a same type, e.g. time-year and time-day)

		} ).map( function( filter ) {
			categoryFilters.oouiWidgetsFilters[filter[0]] = filter;
			return filter;
		} );

		var searchButtonWidget = new OO.ui.ButtonWidget( {
			label: msg( 'button-search' ),
			flags: [
				'primary',
				'progressive'
			]
		} );
		searchButtonWidget.on( 'click', launchSearch );

		var resetButtonWidget = new OO.ui.ButtonWidget( {
			label: msg( 'button-reset' ),
			icon: 'trash',
			flags: [
				'primary',
				'destructive'
			]
		} );
		resetButtonWidget.on( 'click', function() {
			categoryFilters.listFilters.map( function( filter ) {
				categoryFilters.oouiWidgetsFilters[filter][3].map( function( oouiWidget ) {
					if( oouiWidget.menu ) {
						oouiWidget.getMenu().selectItem();
					}
					oouiWidget.setValue( [] );
				} );
			} );
			$( '#category-filters-total-filtered' ).html( '' );
			if( $( '#mw-pages #category-filter-results' ).length ) {
				$( '#mw-pages #category-filter-results' ).detach();
				$( '#mw-pages div.category-filter-wrap > *' ).unwrap();
				$( '#mw-pages h2:first-of-type' ).detach();
			}
			$( 'span.category-filters-unknown-property' ).detach();
			$( 'span.category-filters-unknown-qid' ).detach();
			$( '#mw-pages li' ).each( function() {
				$( this ).removeClass( 'hidden-by-filter' );
			} );
			//$( '#mw-pages li .category-filters-unknown-qid' ).each( function() {
			//	$( this ).addClass( 'hidden-by-default' );
			//} );
			$( '#mw-pages div.mw-category-group' ).each( function() {
				$( 'h3', this ).removeClass( 'hidden-by-filter' );
				$( this ).removeClass( 'hidden-by-filter' );
			} );
			$( '#mw-pages > div:first-of-type' ).css( 'display', 'block' );
			$( '#category-filter-all-pages' ).css( 'display', 'none' );
		} );

		var partialResultsSwitch = new OO.ui.ToggleSwitchWidget( { value: true } );
		partialResultsSwitch.on( 'change', function( visibility ) {
			$( 'li[data-wikidata]' ).each( function( i, elem ) {
				if( $( '.category-filters-unknown-property', elem ).length ) {
					if( visibility ) {
						$( elem ).removeClass( 'hidden-by-filter' );
					} else {
						$( elem ).addClass( 'hidden-by-filter' );
					}
				}
			} );
			$( 'div.mw-category-group' ).each( function( i, elem ) {
				if( $( 'li', elem ).length === $( 'li.hidden-by-filter', elem ).length ) {
					$( 'h3', elem ).addClass( 'hidden-by-filter' );
				} else {
					$( 'h3', elem ).removeClass( 'hidden-by-filter' );
				}
			} );
		} );

		$( '#category-filters' ).append( '<h2>' + msg( 'title-search-filters' ) + '</h2>' );
		oouiWidgetsFiltersList.map( function( widget ) {

			var	filter = widget[0],
				type = widget[1],
				label = widget[2][0],
				oouiWidget = widget[3];
			$( '#category-filters' ).append( '<div id="filter-' + filter + '" class="filter filter-type-' + type + '"><div class="category-filter-label"><label>' + label + '</label></div><span class="widget-filter"></span></div>' );
			oouiWidget.map( function( x, i ) {
				if( i+1 < widget[2].length ) { 
					$( '#filter-' + filter + ' span.widget-filter' ).append( widget[2][i+1] );
				}
				$( '#filter-' + filter + ' span.widget-filter' ).append( x.$element );
			} );
		} );
		$( '#category-filters' ).append( searchButtonWidget.$element, resetButtonWidget.$element );
		$( '#category-filters' ).append( '<span id="category-filter-spinner"></span>' );
		$( '#category-filter-spinner' ).append( widgetSpinner.$element );
		if( categoryFilters.searchStrategy === 'online' ) {
			$( '#category-filter-spinner' ).addClass( 'category-filter-hidden' );
		}
		$( '#category-filters' ).append( '<div id="category-filter-advanced"></div>' );
		if( !isBigCategory ) {
			$( '#category-filter-advanced' ).append( partialResultsSwitch.$element );
			$( '#category-filter-advanced' ).append( msg( 'switch-partial-results' ) + '<br />' );
		}
		$( '#category-filter-advanced' ).append( 'Advanced: <a id="category-filter-link-sparql" href="" target="_blank">SPARQL request for the whole category <img src="/w/skins/Vector/resources/common/images/link-external-small-ltr-progressive.svg" alt="External link" /></a></div>' );
		$( '#category-filter-link-sparql' ).attr( 'href', 'https://query.wikidata.org/#' + encodeURIComponent( categoryFilters.sparqlQuery.replace( /\n  # OPTIONAL ONLINE QUERY/, '' ) ) );
		$( '#mw-pages > p' ).append( '<span id="category-filters-total-filtered"></span>' );

		initialised = true;
		events.emit( 'initialised' );

		// Append to the wrapper
		//$( '#category-filters' ).html( '<div class="wrapper" style="width: 300px"><h3>Search among biographies</h3><span id="birthYearMin">Born in (year) between </span> and (optionnal) <span id="birthYearMax"> </span> And (logical) <span id="andLogical"> </span> Born in (city) <span id="birthPlaceCountry"> </span></div>' );
		//$( '#birthYearMin' ).append(birthYearMin.$element);
		//$( '#birthYearMax' ).append(birthYearMax.$element);
		//$( '#andLogical' ).append(switchButton.$element);
		//$( '#birthPlaceCountry' ).append(birthPlaceCountry.$element);
		//$( '.wrapper').append(
		//	searchButtonWidget.$element,
		//	resetButtonWidget.$element
		//);
	}
	
	function launchSearch() {
		console.log( 'Search' );
		if( categoryFilters.searchStrategy === 'online' ) {
			console.debug( 'Search online' );
			var sparqlValues = categoryFilters.listFilters.map( function( filter ) {
				console.debug( [ filter, properties[filter], categoryFilters.oouiWidgetsFilters[filter][3][0].getValue(), categoryFilters.oouiWidgetsFilters[filter][3].length > 1 ? categoryFilters.oouiWidgetsFilters[filter][3][1].getValue() : undefined ] );
				if( properties[filter].type === 'WikibaseItem' && categoryFilters.oouiWidgetsFilters[filter][3][0].getValue().length ) {
					return '  ?item wdt:' + filter + ' ?values' + filter + ' . VALUES ?values' + filter + ' { ' + categoryFilters.oouiWidgetsFilters[filter][3][0].getValue().filter( function( filter ) { return !!filter; } ).map( function( qid ) { return 'wd:' + qid; } ).join( ' ' ) + ' } .\n';
				} else if( properties[filter].type === 'Time' && ( categoryFilters.oouiWidgetsFilters[filter][3][0].getValue() || categoryFilters.oouiWidgetsFilters[filter][3][1].getValue() ) ) {
					var valueTime = '';
					if( categoryFilters.oouiWidgetsFilters[filter][3][0].getValue().match( /^-?[0-9]+$/ ) ) {
						valueTime += '  FILTER( ?' + filter + ' >= "' + categoryFilters.oouiWidgetsFilters[filter][3][0].getValue() + '-01-01T00:00:00Z"^^xsd:dateTime ) .\n';
					}
					if( categoryFilters.oouiWidgetsFilters[filter][3][1].getValue().match( /^-?[0-9]+$/ ) ) {
						valueTime += '  FILTER( ?' + filter + ' <= "' + categoryFilters.oouiWidgetsFilters[filter][3][1].getValue() + '-01-01T00:00:00Z"^^xsd:dateTime ) .\n';
					}
					return valueTime;
				} else if( properties[filter].type === 'Quantity' && ( categoryFilters.oouiWidgetsFilters[filter][3][0].getValue() || categoryFilters.oouiWidgetsFilters[filter][3][1].getValue() ) ) {
					var valueQuantity = '';
					if( categoryFilters.oouiWidgetsFilters[filter][3][0].getValue().match( /^-?[0-9]+(?:[,.][0-9]+)?$/ ) ) {
						valueQuantity += '  FILTER( ?' + filter + ' >= ' + categoryFilters.oouiWidgetsFilters[filter][3][0].getValue() + ' ) .\n';
					}
					if( categoryFilters.oouiWidgetsFilters[filter][3][1].getValue().match( /^-?[0-9]+(?:[,.][0-9]+)?$/ ) ) {
						valueQuantity += '  FILTER( ?' + filter + ' <= ' + categoryFilters.oouiWidgetsFilters[filter][3][1].getValue() + ' ) .\n';
					}
					return valueQuantity;
				}
				console.debug( 'unimplemented SPARQL datatype "' + properties[filter].type + '' );
				return ''; // TODO for other types
			} ).filter( function( filter ) { return !!filter; } );
			if( sparqlValues.length === 0 ) {
				return;
			}
			var sparql = categoryFilters.sparqlQuery.replace( /  # OPTIONAL ONLINE QUERY/, sparqlValues.join( '\n' ) );
			console.log( sparql );
			$( '#category-filter-spinner' ).removeClass( 'category-filter-hidden' );
			var updateTextSpinner = function( iter, end ) {
				console.debug( 'Progress', end ? 100 : iter*100 / numberOfArticles );
				widgetSpinner.setProgress( end ? 100 : iter*100 / numberOfArticles * 100 );
				//$( '#category-filter-spinner-text' ).text( ( end ? numberOfArticles : iter*100 ) + '/' + numberOfArticles );
			};
			getSPARQLResultsWithProgressBar( sparql, updateTextSpinner, 100, 500, 60000 ).then( function( x ) {
				console.log( x );
				$( '#category-filter-spinner' ).addClass( 'category-filter-hidden' );
				var pages = x.results.bindings.map( function( item ) {
					var title = mw.Title.newFromText( decodeURIComponent( item.sitelink.value ).replace( /^https:\/\/[a-z-]+\.wikipedia\.org\/wiki\//, '' ) );
					return '<li data-wikidata="' + item.item.value.substr( 31 ) + '"><a href="' + item.sitelink.value + '" title="' + title.getPrefixedText() + '">' + title.getPrefixedText() + '</a></li>';
				} ).filter( unique );
				if( $( '#mw-pages #category-filter-results' ).length ) {
					$( '#category-filters-total-filtered' ).html( '' );
					if( $( '#mw-pages #category-filter-results' ).length ) {
						$( '#mw-pages #category-filter-results' ).detach();
						$( '#mw-pages div.category-filter-wrap > *' ).unwrap();
						$( '#mw-pages h2:first-of-type' ).detach();
					}
				}
				$( '#mw-pages' ).wrapInner( '<div class="category-filter-wrap"></div>' );
				$( '#mw-pages div.category-filter-wrap h2' ).clone().prependTo( $( '#mw-pages' ) );
				$( '#mw-pages' ).append( '<div id="category-filter-results"><div class="mw-category mw-category-columns"><div class="mw-category-group">' + ( pages.length ? msg( 'total-pages-displayed' ).replace( '$1', pages.length ) + '<ul>' + pages.join( '' ) + '</ul>' : msg( 'no-results' ) ) + '</div></div></div>' );
			} );
			return;
		}
		subjectQids = categoryFilters.allSubjects.reduce( function( acc, x ) {
			acc[x] = true;
			return acc;
		}, {} );
		if( categoryFilters.allSubjects === null ) {
			console.log( 'Too early: we did not have the Wikidata data for now. Please retry later.' );
			return;
		}
		categoryFilters.listFilters.map( function( filter ) {
			subjectQids = applyFilterFirstPass( filter, subjectQids );
		} );
		categoryFilters.listFilters.map( function( filter ) {
			subjectQids = applyFilterSecondPass( filter, subjectQids );
		} );
		console.log( subjectQids );
		var displayedPages = 0;
		if( $( '#mw-pages > a' ).length === 0 ) {
			$( '#mw-pages li' ).each( function() {
				var thjs = $( this ),
					qid = thjs.attr( 'data-wikidata' );
				if( subjectQids[qid] === true ) {
					$( 'span.category-filters-unknown-qid', thjs ).addClass( 'hidden-by-default' );
					$( 'span.category-filters-unknown-property', thjs ).addClass( 'hidden-by-default' );
					thjs.removeClass( 'hidden-by-filter' );
					displayedPages++;
				} else if( !qid ) {
					$( 'span.category-filters-unknown-property', thjs ).addClass( 'hidden-by-default' );
					$( 'span.category-filters-unknown-qid', thjs ).removeClass( 'hidden-by-default' );
					thjs.removeClass( 'hidden-by-filter' );
					displayedPages++;
				} else if( subjectQids[qid] === noProperty ) {
					if( !$( 'span.category-filters-unknown-property', thjs ).length ) {
						$( this ).append( '<span class="category-filters-unknown-property hidden-by-default" title="' + msg( 'tooltip-missing-property' ) + '"> <img src="//upload.wikimedia.org/wikipedia/commons/2/20/Adobe_RoboInfo_5_icon_-_2.png" /></span>' );
					}
					$( 'span.category-filters-unknown-qid', thjs ).addClass( 'hidden-by-default' );
					$( 'span.category-filters-unknown-property', thjs ).removeClass( 'hidden-by-default' );
					thjs.removeClass( 'hidden-by-filter' );
					displayedPages++;
				} else {
					thjs.addClass( 'hidden-by-filter' );
				}
			} );
			$( '#category-filters-total-filtered' ).html( ' ' + msg( 'total-pages-displayed' ).replace( '$1', displayedPages ) );
			$( '#mw-pages div.mw-category-group' ).each( function() {
				var total = $( 'li', this ).length,
					hidden = $( 'li.hidden-by-filter', this ).length;
				if( total === hidden ) {
					$( this ).addClass( 'hidden-by-filter' );
				} else {
					$( this ).removeClass( 'hidden-by-filter' );
				}
			} );
		} else {
			$( '#mw-pages > div:first-of-type' ).css( 'display', 'none' );
			if( $( '#category-filter-all-pages' ).length === 0 ) {
				//var headerRegex = new RegExp( '^([ \n]*<h2.*?</h2>)(.*?)(<div dir="[^"]*" class="[^"]*" lang="[^"]*"><div class="mw-category">)', 's' );
				//$( '#mw-pages' ).html( $( '#mw-pages' ).html().replace( headerRegex, '$1<div class="category-filters-intro-category">$2</div>$3' ) );
				$( '#mw-pages' ).append( '<div id="category-filter-all-pages" class="mw-content-ltr" dir="ltr" lang="it"></div>' );
			}
			$( '.category-filters-intro-category' ).css( 'display', 'none' );
			var items = Object.keys( subjectQids ).map( function( x ) {
				if( subjectQids[x] === true ) {
					return '<li data-wikidata="' + x + '"><a href="' + categoryFilters.sitelinks[x] + '">' + decodeURI( categoryFilters.sitelinks[x] ).replace( /^https?:\/\/[a-z-]+\.wikipedia\.org\/wiki\//, '' ).replace( /_/g, ' ' ).replace( /%3F/g, '?' ) + '</a></li>';
				} else if( subjectQids[x] === noProperty ) {
					return '<li data-wikidata="' + x + '"><a href="' + categoryFilters.sitelinks[x] + '">' + decodeURI( categoryFilters.sitelinks[x] ).replace( /^https?:\/\/[a-z-]+\.wikipedia\.org\/wiki\//, '' ).replace( /_/g, ' ' ).replace( /%3F/g, '?' ) + '</a><span class="category-filters-unknown-property" title="' + msg( 'tooltip-missing-property' ) + '"> <img src="//upload.wikimedia.org/wikipedia/commons/2/20/Adobe_RoboInfo_5_icon_-_2.png" /></span></li>';
				}
			} );
			$( '#category-filter-all-pages' ).html( '<div class="mw-category"><div class="mw-category-group"><ul>' + items.join( '' ) + '</ul></div></div>' );
			$( '#category-filter-all-pages' ).css( 'display', 'block' );
		}
		return false;
	}
	
	/**
	 * Add the attribute data-wikidata to <li> tag in the pages of the category.
	 *
	 * It requires the object categoryFilters.articlesToWikidata to be populated beforehand.
	 */
	function addQidToCategoryPages() {
		if( !categoryFilters.articlesToWikidata || Object.values( categoryFilters.articlesToWikidata ) === 0 ) {
			return;
		}
		$( '#mw-pages li' ).each( function() {
			var a = $( 'a', this ),
				href = a.attr( 'href' );
			if( categoryFilters.articlesToWikidata[href] ) {
				$( this ).attr( 'data-wikidata', categoryFilters.articlesToWikidata[href] );
			} else {
				$( this ).attr( 'data-wikidata', '' ).append( '<span class="category-filters-unknown-qid hidden-by-default" title="' + msg( 'tooltip-missing-wikidata' ) + '"> <img src="//upload.wikimedia.org/wikipedia/commons/6/6b/Adobe_RoboInfo_5_icon_-_2-red.png" /></span>' );
			}
		} );
	}

	/**
	 * Apply the first pass of one filter to a set of pages, which adds the status "noProperty" to pages if the filter is filled but the page has no corresponding property.
	 */
	function applyFilterFirstPass( filter, subjectQids ) {
		var property = categoryFilters.oouiWidgetsFilters[filter][0],
			type = categoryFilters.oouiWidgetsFilters[filter][1],
			oouiWidgets = categoryFilters.oouiWidgetsFilters[filter][3];
		if( !categoryFilters.cachePV[property] ) {
			categoryFilters.cachePV[property] = {};
		}
		if( oouiWidgets.length === 1 && type === 'wikibase-item' ) {
			var oouiWidget = oouiWidgets[0],
				values = oouiWidget.getValue();
			if( values.length === 0 ) {
				return subjectQids;
			}
		} else if( oouiWidgets.length === 2 && type === 'time' ) { // year
			var minYear = oouiWidgets[0].getValue() ? parseInt( oouiWidgets[0].getValue() ) : null,
				maxYear = oouiWidgets[1].getValue() ? parseInt( oouiWidgets[1].getValue() ) : null;
			if( ( minYear === null || isNaN( minYear ) ) && ( maxYear === null || isNaN( maxYear ) ) ) {
				return subjectQids;
			}
		} else if( oouiWidgets.length === 2 && type === 'quantity' ) { // quantity
			var minQuantity = oouiWidgets[0].getValue() ? parseFloat( oouiWidgets[0].getValue() ) : null,
				maxQuantity = oouiWidgets[1].getValue() ? parseFloat( oouiWidgets[1].getValue() ) : null;
			if( ( minQuantity === null || isNaN( minQuantity ) ) && ( maxQuantity === null || isNaN( maxQuantity ) ) ) {
				return subjectQids;
			}
		} else if( oouiWidgets.length === 1 && type === 'string' ) { // string
			var stringValue = oouiWidgets[0].getValue();
			if( !stringValue ) {
				return subjectQids;
			}
		} else {
			console.debug( 'unimplemented applyFilterFirstPass() unknown type' );
			return subjectQids;
		}

		// Add items for which the property is not present: we cannot filter on this property so they have the status "noProperty"
		if( !categoryFilters.cachePV[property]['no-value'] ) {
			categoryFilters.cachePV[property]['no-value'] = categoryFilters.valuesFromWikidataLight.filter( function( x ) {
				return !x[property];
			} ).map( function( x ) {
				return x.item;
			} );
		}
		categoryFilters.cachePV[property]['no-value'].map( function( x ) {
			subjectQids[x] = noProperty;
		} );

		return subjectQids;
	}
	
	/**
	 * Apply the second pass of one filter to a set of pages, which removes pages if the filter is filled and the corresponding property is declared for the page and the page does not have some searched value.
	 */
	function applyFilterSecondPass( filter, subjectQids ) {
		var property = categoryFilters.oouiWidgetsFilters[filter][0],
			type = categoryFilters.oouiWidgetsFilters[filter][1],
			oouiWidgets = categoryFilters.oouiWidgetsFilters[filter][3],
			matchingItems = [];
		if( !categoryFilters.cachePV[property] ) {
			categoryFilters.cachePV[property] = {};
		}
		if( oouiWidgets.length === 1 && type === 'wikibase-item' ) {
			var oouiWidget = oouiWidgets[0],
				values = oouiWidget.getValue();
			if( values.length === 0 ) {
				return subjectQids;
			}
			values.map( function( value ) {
				if( !categoryFilters.cachePV[property][value] ) {
					categoryFilters.cachePV[property][value] = categoryFilters.valuesFromWikidataLight.filter( function( x ) {
						return x[property] === value;
					} ).map( function( x ) {
						return x.item;
					} );
				}
				matchingItems = matchingItems.concat( categoryFilters.cachePV[property][value] );
			} );
		} else if( oouiWidgets.length === 2 && type === 'time' ) { // year
			var minYear = oouiWidgets[0].getValue() ? parseInt( oouiWidgets[0].getValue() ) : null,
				maxYear = oouiWidgets[1].getValue() ? parseInt( oouiWidgets[1].getValue() ) : null;
			if( ( minYear === null || isNaN( minYear ) ) && ( maxYear === null || isNaN( maxYear ) ) ) {
				return subjectQids;
			}
			matchingItems = categoryFilters.valuesFromWikidataLight.filter( function( x ) {
				if( !x[property] ) {
					return false;
				}
				var year = parseInt( x[property].substr( 0, 4 ) );
				return ( minYear === null || minYear <= year ) && ( maxYear === null || year <= maxYear );
			} ).map( function( x ) {
				return x.item;
			} );
		} else if( oouiWidgets.length === 2 && type === 'quantity' ) { // quantity
			var minQuantity = oouiWidgets[0].getValue() ? parseFloat( oouiWidgets[0].getValue() ) : null,
				maxQuantity = oouiWidgets[1].getValue() ? parseFloat( oouiWidgets[1].getValue() ) : null;
			if( ( minQuantity === null || isNaN( minQuantity ) ) && ( maxQuantity === null || isNaN( maxQuantity ) ) ) {
				return subjectQids;
			}
			matchingItems = categoryFilters.valuesFromWikidataLight.filter( function( x ) {
				if( !x[property] ) {
					return false;
				}
				var quantity = parseFloat( x[property] );
				return ( minQuantity === null || minQuantity <= quantity ) && ( maxQuantity === null || quantity <= maxQuantity );
			} ).map( function( x ) {
				return x.item;
			} );
		} else if( oouiWidgets.length === 1 && type === 'string' ) { // string
			var stringValue = oouiWidgets[0].getValue() ? oouiWidgets[0].getValue() : null;
			if( !stringValue ) {
				return subjectQids;
			}
			matchingItems = categoryFilters.valuesFromWikidataLight.filter( function( x ) {
				if( !x[property] ) {
					return false;
				}
				return x[property].toLowerCase().includes( stringValue.toLowerCase() );
			} ).map( function( x ) {
				return x.item;
			} );
		} else {
			console.debug( 'unimplemented applyFilterSecondPass() unknown type' );
			return subjectQids;
		}

		// We remove from the result the items which does not match this filter AND have the corresponding property
		// Because if the corresponding property is not present, it cannot vote and the item must keep its value "noProperty"
		Object.keys( subjectQids ).map( function( x ) {
			if( !matchingItems.includes( x ) && !categoryFilters.cachePV[property]['no-value'].includes( x ) ) {
				delete subjectQids[x];
			}
		} );

		return subjectQids;
	}

	/**
	 * Retrieve SPARQL results with a progress bar.
	 *
	 * @param string query SPARQL query
	 * @param callback clb Function to call when intermediary results are obtained, its prototype is function( Number iter, bool end)
	 * @param Number? bucketSize Number of articles in each bucket (1000 by default, max 1000)
	 * @param Number? maxContinuations Hard limit of number of continuations (50 by default, max 50)
	 * @param Number? timeout Timeout of each request in milliseconds (60 seconds by default, max 60 seconds)
	 * @return Promise<Object> When the promise is solved, SPARQL result
	 */
	function getSPARQLResultsWithProgressBar( query, clb, bucketSize, maxContinuations, timeout ) {

		maxContinuations = maxContinuations === undefined || maxContinuations > 500 ? 500 : maxContinuations;
		bucketSize = bucketSize === undefined || bucketSize > 1000 ? 1000 : bucketSize;
		timeout = timeout === undefined || timeout > 60000 ? 60000 : timeout;
		query = query.replace( /\?itemLabel/, '$& ?continuation' ).replace( /mwapi:inprop "url";/, '$& wikibase:limit "once"; mwapi:gcmlimit ' + bucketSize + '; mwapi:gcmcontinue "#PLACEHOLDER-CONTINUE#"' ).replace( /    \?sitelink wikibase:apiOutput "@fullurl"/, '$& .\n    ?continuation wikibase:apiOutput "/api/continue/@gcmcontinue"' );
		var resultHead, resultBindings = [];

		var promiseResult = function( continuation, iterContinuations ) {

			clb( iterContinuations, false );
			var thisquery = query.replace( /#PLACEHOLDER-CONTINUE#/, continuation.replaceAll( /\\/g, '\\\\' ).replaceAll( /"/g, '\\"' ) ); // escape backslashes and double-quote to prevent SPARQL injections
			return $.getJSON( { url: 'https://query.wikidata.org/bigdata/namespace/wdq/sparql', timeout: timeout }, { query: thisquery } ).then( function( result ) {

				if( !result || !result.head || !result.results || !result.results.bindings ) {
					clb( iterContinuations, true );
					return { head: resultHead, results: { bindings: resultBindings } };
				} else {
					if( result.head.vars && Array.isArray( result.head.vars ) ) {
						result.head.vars = result.head.vars.filter( function( x ) {
							return x !== 'continuation';
						} );
					}
					resultHead = result.head;
				}

				if( result.results.bindings.length === 0 || !result.results.bindings[0] ) {
					// SPARQL-MWAPI did not return any result, so we don’t know what is the
					// continuation value and we are unable to continue the search, except
					// by issuing a dedicated API call outside of SPARQL-MWAPI just to obtain
					// this continuation value. This is a quite bad work-around but I did not
					// find a better solution.
					var api = new mw.Api();
					return api.get( {
						action: 'query',
						generator: 'categorymembers',
						gcmtitle: mw.config.get( 'wgPageName' ),
						gcmlimit: bucketSize,
						gcmcontinue: continuation,
					} ).then( function( data ) {
						if( data && data.continue && data.continue.gcmcontinue ) {
							var newContinuation = data.continue.gcmcontinue;
							return promiseResult( newContinuation, iterContinuations+1 );
						} else {
							return { head: resultHead, results: { bindings: resultBindings } };
						}
					} );
				}
				var newContinuation = result.results.bindings[0].continuation && result.results.bindings[0].continuation.value ? result.results.bindings[0].continuation.value : false;
				resultBindings = resultBindings.concat( result.results.bindings.map( function( x ) {
					delete x.continuation;
					return x;
				} ) );
				if( !newContinuation || iterContinuations > maxContinuations ) {
					clb( iterContinuations, true );
					return { head: resultHead, results: { bindings: resultBindings } };
				}
				return promiseResult( newContinuation, iterContinuations+1 );
			} );
		};

		return promiseResult( '', 0 );
	}

	/**
	 * Retrieve basic metadata for properties.
	 *
	 * Security: this function handles sanitisation of the parameters.
	 * Global variables: isBigCategory, properties
	 * Used functions: getPropertyMetadata, getAuthorisedValues
	 *
	 * @param string[] listProperties List of properties, each one must be ^P[0-9]+$
	 * @param string languages List of language codes (the latters are fallbacks of the formers), e.g. "hi,en,it"
	 * @return Promise<void> When the promise is solved, all metadata for the properties were put in the global object `properties`
	 */
	function getPropertiesMetadata( listProperties, languages ) {

		listProperties = listProperties.filter( function( property ) {
			return /^P[0-9]+$/.test( property );
		} );

		return Promise.all( listProperties.map( function( property ) {
			// initialise the description of the property, but do not override if already defined
			if( !properties[property] ) {
				properties[property] = {
					type: undefined,
					label: undefined,
					level: 0,
				};
			}
			// if the category already reached the maximum level, do not recompute it (it is the case if the definition of `properties` statically defines the values)
			if( properties[property].level === 4 ) {
				return;
			}
			// TODO discuss and evaluate what should be considered a “big category” (the current condition says 200 articles)
			if( properties[property].level === 3 && isBigCategory ) {
				return;
			}
			return getPropertyMetadata( property, languages ).then( function( x ) {
				properties[property] = {
					type: x.type,
					label: x.label,
					level: 1,
				};
				if( x.type === 'String' || x.type === 'Monolingualtext' || x.type === 'Url' || x.type === 'ExternalId' ) {
					if( x.regex !== undefined ) {
						properties[property].level = 2;
						properties[property].regex = x.regex;
					}
				} else if( x.type === 'Quantity' || x.type === 'Time' ) {
					if( x.min !== undefined ) {
						properties[property].level = 2;
						properties[property].min = x.min;
					}
					if( x.max !== undefined ) {
						properties[property].level = 2;
						properties[property].max = x.max;
					}
				} else if( x.type === 'WikibaseItem' && x.imageCard && !properties[property].values ) {
					return getAuthorisedValues( property, languages ).then( function( y ) {
						// do not override level-4 values if they became available
						if( !properties[property].values ) {
							properties[property].level = 3;
							properties[property].values = y;
						}
					} ).fail( function( y ) {
						if( !properties[property].values ) {
							properties[property].values = null;
						}
					} );
				}
			} );
		} ) );
	}

	/**
	 * Get authorised values of a property with a “one-of” constraint.
	 *
	 * Security warning: the parameters are not escaped, do proper escaping before use.
	 * Global variables: -
	 * Used functions: $.getJSON
	 *
	 * TODO return the values of the qualifier “exception to contraint” (P2303), very rare but currently used on P91, P517, P853, P9971
	 *
	 * @param string Pid Property ID, e.g. "P21" or "P577"
	 * @param string languages List of language codes (the latters are fallbacks of the formers), e.g. "hi,en,it"
	 * @return Promise<string[][]|null> Promise returning a list of authorised values or null if there is no contraint “one-of”; each value is an object { item: (string), lang: (string), label: (string) ]
	 */
	function getAuthorisedValues( Pid, languages ) {

		var sparql = 'SELECT ?item ?itemLabel WHERE { wd:' + Pid + ' wikibase:propertyType wikibase:WikibaseItem ; p:P2302 [ ps:P2302 wd:Q21510859 ; pq:P2305 ?item ] . SERVICE wikibase:label { bd:serviceParam wikibase:language "' + languages + '" } }';
		return $.getJSON( 'https://query.wikidata.org/bigdata/namespace/wdq/sparql', { query: sparql } ).then( function( x ) {
				if( !x || !x.results || !x.results.bindings || !Array.isArray( x.results.bindings ) ) {
					return null; // TODO return error?
				}
				if( x.results.bindings.length === 0 ) {
					return null;
				}
				return x.results.bindings.map( function( y ) {
						if( !y || !y.item || !y.itemLabel || y.item.type !== 'uri' || y.itemLabel.type !== 'literal' || y.item.value.substr( 0, 31 ) !== 'http://www.wikidata.org/entity/' ) {
							return null;
						}
						return { item: y.item.value.substr( 31 ), lang: y.itemLabel['xml:lang'], label: y.itemLabel.value };
					} ).filter( function( y ) {
						return !!y;
					} );
			} );
	}

	/**
	 * Try to get all values of a property (the image of the property).
	 *
	 * Security warning: the parameters are not escaped, do proper escaping before use.
	 * Warning: this can be very expansive and long for most properties: only launch this function when you are confident enough there are only a few values (e.g. chemical elements (P246)).
	 * Global variables: -
	 * Used functions: $.getJSON
	 *
	 * @param string Pid Property ID, e.g. "P21" or "P577"
	 * @param Number timeout Timeout for the request in milliseconds; there is a hard limit to 5 seconds
	 * @return Promise<string[]|null> Promise returning a list of all values or null if we cannot obtain this list
	 */
	function getAllValues( Pid, timeout ) {

		var sparql = 'SELECT ?value WHERE { [] wdt:' + Pid + ' ?value }';
		if( !timeout || timeout <= 0 || timeout >= 5000 ) {
			timeout = 5000;
		}
		return $.getJSON( { url: 'https://query.wikidata.org/bigdata/namespace/wdq/sparql', timeout: timeout }, { query: sparql } ).then( function( x ) {
				if( !x || !x.results || !x.results.bindings || !Array.isArray( x.results.bindings ) ) {
					return null; // TODO return error?
				}
				return x.results.bindings.map( function( y ) {
						// TODO should we manage "unknown value"?
						if( !y || !y.value ) {
							return null;
						}
						return y.value.value;
					} ).filter( function( y ) {
						return !!y;
					} );
			} );
	}

	/**
	 * Obtain basic metadata about a property.
	 *
	 * Security warning: the parameters are not escaped, do proper escaping before use.
	 * Global variables: -
	 * Used functions: $.getJSON
	 *
	 * @param string Pid Property ID, e.g. "P21" or "P577"
	 * @param string languages List of language codes (the latters are fallbacks of the formers), e.g. "hi,en,it"
	 * @return Promise<Object> Promise returning an object with keys "name" (type string|null) and "type" (type string|null)
	 */
	function getPropertyMetadata( Pid, languages ) {

		// TODO should we enforce use of non-deprecated constraints? see for instance https://www.wikidata.org/wiki/Property:P364#P2302 with “one-of constraint”: currently the list of elements is returned
		var sparql = 'SELECT ?propertyLabel ?type ?min ?max ?regex (COUNT(?value) AS ?imageCard) WHERE { BIND( wd:' + Pid + ' AS ?property ) . ?property wikibase:propertyType ?type . OPTIONAL { ?property p:P2302 [ ps:P2302 wd:Q21510860 ; pq:P2313 ?min ] } . OPTIONAL { ?property p:P2302 [ ps:P2302 wd:Q21510860 ; pq:P2310 ?min ] } . OPTIONAL { ?property p:P2302 [ ps:P2302 wd:Q21510860 ; pq:P2312 ?max ] } . OPTIONAL { ?property p:P2302 [ ps:P2302 wd:Q21510860 ; pq:P2311 ?max ] } . OPTIONAL { ?property p:P2302 [ ps:P2302 wd:Q21502404 ; pq:P1793 ?regex ] } . OPTIONAL { ?property p:P2302 [ ps:P2302 wd:Q21510859 ; pq:P2305 ?value ] } . SERVICE wikibase:label { bd:serviceParam wikibase:language "' + languages + '" } } GROUP BY ?propertyLabel ?type ?min ?max ?regex';
		return $.getJSON( 'https://query.wikidata.org/bigdata/namespace/wdq/sparql', { query: sparql } ).then( function( x ) {
				if( !x || !x.results || !x.results.bindings || !Array.isArray( x.results.bindings ) ) {
					return { type: undefined, label: undefined };
				}
				if( x.results.bindings.length === 0 ) {
					return { type: undefined, label: undefined };
				}
				var metadata = x.results.bindings[0];
				if( metadata.type.type !== 'uri' || metadata.propertyLabel.type !== 'literal' || metadata.type.value.substr( 0, 26 ) !== 'http://wikiba.se/ontology#' ) {
					return { type: undefined, label: undefined };
				}
				return {
						type: metadata.type.value.substr( 26 ),
						label: metadata.propertyLabel ? metadata.propertyLabel.value : undefined,
						min: metadata.min && metadata.min.type === 'literal' ? metadata.min.value : undefined,
						max: metadata.max && metadata.max.type === 'literal' ? metadata.max.value : undefined,
						regex: metadata.regex && metadata.regex.type === 'literal' ? metadata.regex.value : undefined,
						imageCard: metadata.imageCard ? Number( metadata.imageCard.value ) : undefined,
					};
			} );
	}

	function ucFirst( str ) {

		return str.substring( 0, 1 ).toUpperCase() + str.substring( 1 );
	}

	function unique( value, index, self ) {
		return self.indexOf( value ) === index;
	}

	window.globalApplyFilterFirstPass = applyFilterFirstPass; // DEBUG
	window.globalApplyFilterSecondPass = applyFilterSecondPass; // DEBUG
	window.globalgetSPARQLResultsWithProgressBar = getSPARQLResultsWithProgressBar;
} );