wikimedica-disease-search/app/src/components/TermInput.js

211 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 terms Ensemble de termes déjà validés.
* @param availableTerms Termes pouvant être ajoutés par lutilisateur.
* @prop setTerms Fonction de rappel utilisée pour modifier lensemble des
* termes validés par lutilisateur.
*/
const TermInput = ({terms, availableTerms, setTerms}) =>
{
// 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 à lensemble des termes sélectionnés.
*
* @param term Terme à ajouter.
*/
const addTerm = term =>
{
if (terms.some(({id}) => id === term.id))
{
// Najoute pas les termes déjà sélectionnés
return;
}
setTerms(terms.concat([term]));
setValue('');
setSuggestions([]);
if (inputRef.current !== null)
{
inputRef.current.focus();
}
};
/**
* Supprime un terme des termes sélectionnés.
*
* @param index Indice du terme à supprimer dans la liste.
*/
const removeTerm = index =>
{
setTerms([
...terms.slice(0, index),
...terms.slice(index + 1)
]);
};
/**
* Gère lappui sur une touche dans le champ de saisie.
*
* @param ev Informations sur lévénement dappui.
*/
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)
{
addTerm(suggestions[focusedSuggestion]);
}
}
else if (
ev.keyCode === keys.backspace
&& !value
&& terms.length !== 0
)
{
// Touche Retour alors que le champ de saisie est vide :
// retrait du dernier terme
ev.preventDefault();
setTerms(terms.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 nont pas déjà été choisis
availableTerms.filter(({id: suggestionId}) =>
!terms.some(({id: termId}) =>
termId === suggestionId
)
)
));
}
};
return (
<div className="TermInput">
{terms.map((term, index) =>
<span
key={term.id}
className="TermInput_term"
onClick={removeTerm.bind(null, index)}
>
{term.name}
</span>
)}
<input
autoFocus={true}
type="text" className="TermInput_input"
placeholder="Rechercher un symptôme, un signe ou une maladie…"
value={value} ref={inputRef}
onChange={handleInputChange}
onKeyDown={handleInputKeyDown} />
<ul className="TermInput_suggestions">
{suggestions.map((suggestion, index) =>
<li
key={suggestion.id}
className={[
'TermInput_suggestion',
index === focusedSuggestion
? 'TermInput_suggestion-focus'
: ''
].join(' ')}
onMouseEnter={setFocusedSuggestion.bind(null, index)}
onClick={addTerm.bind(null, suggestion)}
>
{suggestion.name}
{suggestion.alias.length >= 1 ? (
<span
className="TermInput_suggestionAlias"
title={
'Alias du terme: '
+ suggestion.alias.join(', ')
}
>
{suggestion.alias.join(', ')}
</span>
) : null}
</li>
)}
</ul>
</div>
);
};
TermInput.propTypes = {
terms: PropTypes.arrayOf(Term).isRequired,
availableTerms: PropTypes.arrayOf(Term).isRequired,
setTerms: PropTypes.func.isRequired
};
export default TermInput;