import React, {useState, useRef} from 'react'; import PropTypes from 'prop-types'; import {Term} from '../data/model.js'; import * as fuzzy from '../data/fuzzy.js'; import * as util from '../util.js'; /** * Codes des touches du clavier par nom. */ const keys = Object.assign(Object.create(null), { 'backspace': 8, 'enter': 13, 'left': 37, 'up': 38, 'right': 39, 'down': 40, }); /** * Zone de saisie des termes de recherche. * * @prop query Ensemble de termes déjà dans la requête. * @prop setQuery Fonction de rappel utilisée pour modifier la requête. * @param availableTerms Termes pouvant être ajoutés par l’utilisateur. */ const TermInput = ({query, setQuery, availableTerms}) => { // Valeur actuellement saisie dans le champ de recherche de termes const [value, setValue] = useState(''); // Liste des termes suggérés par autocomplétion const { list: suggestions, setList: setSuggestions, focus: focusedSuggestion, setFocus: setFocusedSuggestion } = util.useFocusableList(); // Référence au champ de saisie const inputRef = useRef(null); /** * Ajoute un terme à la requête. * * @param term Terme à ajouter. */ const addQueryTerm = term => { if (query.some(({id}) => id === term.id)) { // N’ajoute pas les termes déjà sélectionnés return; } setQuery(query.concat([term])); setValue(''); setSuggestions([]); if (inputRef.current !== null) { inputRef.current.focus(); } }; /** * Supprime un terme de la requête. * * @param index Indice du terme à supprimer dans la liste. */ const removeQueryTerm = index => { setQuery([ ...query.slice(0, index), ...query.slice(index + 1) ]); }; /** * Gère l’appui sur une touche dans le champ de saisie. * * @param ev Informations sur l’événement d’appui. */ const handleInputKeyDown = ev => { if (ev.keyCode === keys.enter && value) { // Touche Entrée : ajout de la suggestion ayant le focus ev.preventDefault(); if (focusedSuggestion < suggestions.length) { addQueryTerm(suggestions[focusedSuggestion]); } } else if ( ev.keyCode === keys.backspace && !value && query.length !== 0 ) { // Touche Retour alors que le champ de saisie est vide : // retrait du dernier terme de la requête ev.preventDefault(); setQuery(query.slice(0, -1)); } else if (ev.keyCode === keys.up) { // Touche Haut : focus de la suggestion précédente ev.preventDefault(); setFocusedSuggestion(focusedSuggestion - 1); } else if (ev.keyCode === keys.down) { // Touche Bas : focus de la suggestion suivante ev.preventDefault(); setFocusedSuggestion(focusedSuggestion + 1); } }; /** * Gère la modification du contenu du champ de saisie. * * @param ev Informations sur l’événement de modification. */ const handleInputChange = ev => { const nextValue = ev.target.value; setValue(nextValue); if (nextValue.length === 0) { // On ne fait pas de suggestion si la valeur saisie est trop courte setSuggestions([]); } else { setSuggestions(fuzzy.filterTerms( // Filtre les termes correspondant à la saisie nextValue, // Sélectionne les termes qui n’ont pas déjà été choisis availableTerms.filter(({id: suggestionId}) => !query.some(({id: termId}) => termId === suggestionId ) ) )); } }; return (
{query.map((term, index) => {term.name} )}
); }; TermInput.propTypes = { query: PropTypes.arrayOf(Term).isRequired, setQuery: PropTypes.func.isRequired, availableTerms: PropTypes.arrayOf(Term).isRequired, }; export default TermInput;