200 lines
5.8 KiB
JavaScript
200 lines
5.8 KiB
JavaScript
import React, {useState, useRef} from 'react';
|
||
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 l’utilisateur.
|
||
* @prop setTerms Fonction de rappel utilisée pour modifier l’ensemble des
|
||
* termes validés par l’utilisateur.
|
||
*/
|
||
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 à l’ensemble des termes sélectionnés.
|
||
*
|
||
* @param term Terme à ajouter.
|
||
*/
|
||
const addTerm = term =>
|
||
{
|
||
if (terms.some(({id}) => id === term.id))
|
||
{
|
||
// N’ajoute 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 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)
|
||
{
|
||
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 n’ont 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>
|
||
);
|
||
};
|
||
|
||
export default TermInput;
|