diff --git a/app/src/components/App.js b/app/src/components/App.js index aa03cff..3e19803 100644 --- a/app/src/components/App.js +++ b/app/src/components/App.js @@ -1,7 +1,7 @@ -import React, {useState} from 'react'; +import React from 'react'; import TermInput from './TermInput.js'; import DiseaseGraph from './DiseaseGraph.js'; -import {useAsync} from '../util.js'; +import {useAsync, useQuery} from '../util.js'; import { diseasesBySymptoms, exploreSymptoms @@ -9,22 +9,35 @@ import { const App = () => { - const [query, setQuery] = useState([]); + const { + query, + addTerm: addQueryTerm, + removeTerm: removeQueryTerm + } = useQuery(); + // Récupération des résultats de la recherche const diseases = useAsync([], diseasesBySymptoms, query); const terms = useAsync([], exploreSymptoms, diseases); + // Tous les termes compatibles avec la recherche qui n’apparaissent pas + // dans la recherche + const availableTerms = terms.filter(({id}) => + !query.some(({id: termId}) => termId === id) + ); + return (
); diff --git a/app/src/components/DiseaseGraph.js b/app/src/components/DiseaseGraph.js index 1427c18..2978bb4 100644 --- a/app/src/components/DiseaseGraph.js +++ b/app/src/components/DiseaseGraph.js @@ -10,10 +10,15 @@ import {symptomsSubgraph} from '../data/mock'; * * @prop terms Termes à afficher. * @prop query Ensemble de termes recherchés par l’utilisateur. - * @prop setQuery Fonction de rappel pour ajouter de nouveaux termes de - * recherche. + * @prop addQueryTerm Fonction de rappel pour ajouter un terme à la requête. + * @prop removeQueryTerm Fonction de rappel pour ôter un terme de la requête. */ -const DiseaseGraph = ({terms, query, setQuery}) => +const DiseaseGraph = ({ + terms, + query, + addQueryTerm, + removeQueryTerm, +}) => { const {nodes, edges} = useAsync({ nodes: {}, @@ -40,7 +45,11 @@ const DiseaseGraph = ({terms, query, setQuery}) => ].join(' ')} title={ `Cliquez pour plus d’informations sur « ${term.name} »\n` - + '(Ctrl-clic pour l’ajouter à la requête)' + + `(Ctrl-clic pour ${ + isInQuery + ? 'l’enlever de' + : 'l’ajouter à' + } la requête)` } > {nodes[id].name} @@ -67,15 +76,12 @@ const DiseaseGraph = ({terms, query, setQuery}) => else if (queryIndex >= 0) { // Ctrl-clic : Retrait d’un terme déjà dans la requête - setQuery([ - ...query.slice(0, queryIndex), - ...query.slice(queryIndex + 1) - ]); + removeQueryTerm(queryIndex); } else { // Ctrl-clic : Ajout d’un nouveau terme dans la requête - setQuery(query.concat([term])); + addQueryTerm(term); } }; @@ -93,7 +99,8 @@ const DiseaseGraph = ({terms, query, setQuery}) => DiseaseGraph.propTypes = { terms: PropTypes.arrayOf(Term).isRequired, query: PropTypes.arrayOf(Term).isRequired, - setQuery: PropTypes.func.isRequired, + addQueryTerm: PropTypes.func.isRequired, + removeQueryTerm: PropTypes.func.isRequired, }; export default DiseaseGraph; diff --git a/app/src/components/TermInput.js b/app/src/components/TermInput.js index 35eb5f2..10a5f7a 100644 --- a/app/src/components/TermInput.js +++ b/app/src/components/TermInput.js @@ -20,10 +20,16 @@ const keys = Object.assign(Object.create(null), { * 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. + * @prop addQueryTerm Fonction de rappel pour ajouter un terme à la requête. + * @prop removeQueryTerm Fonction de rappel pour ôter un terme de la requête. + * @prop availableTerms Termes pouvant être ajoutés par l’utilisateur. */ -const TermInput = ({query, setQuery, availableTerms}) => +const TermInput = ({ + query, + addQueryTerm, + removeQueryTerm, + availableTerms, +}) => { // Valeur actuellement saisie dans le champ de recherche de termes const [value, setValue] = useState(''); @@ -40,19 +46,13 @@ const TermInput = ({query, setQuery, availableTerms}) => const inputRef = useRef(null); /** - * Ajoute un terme à la requête. + * Valide l’entrée courante en l’ajoutant comme terme de la requête. * - * @param term Terme à ajouter. + * @param id Identifiant du terme à ajouter dans la requête. */ - const addQueryTerm = term => + const finalizeInput = id => { - if (query.some(({id}) => id === term.id)) - { - // N’ajoute pas les termes déjà sélectionnés - return; - } - - setQuery(query.concat([term])); + addQueryTerm(id); setValue(''); setSuggestions([]); @@ -62,19 +62,6 @@ const TermInput = ({query, setQuery, availableTerms}) => } }; - /** - * 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. * @@ -89,19 +76,15 @@ const TermInput = ({query, setQuery, availableTerms}) => if (focusedSuggestion < suggestions.length) { - addQueryTerm(suggestions[focusedSuggestion]); + finalizeInput(suggestions[focusedSuggestion]); } } - else if ( - ev.keyCode === keys.backspace - && !value - && query.length !== 0 - ) + else if (ev.keyCode === keys.backspace && !value) { // 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)); + removeQueryTerm(query.length - 1); } else if (ev.keyCode === keys.up) { @@ -134,17 +117,8 @@ const TermInput = ({query, setQuery, availableTerms}) => } 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 - ) - ) - )); + // Filtre les termes correspondant à la saisie + setSuggestions(fuzzy.filterTerms(nextValue, availableTerms)); } }; @@ -180,7 +154,7 @@ const TermInput = ({query, setQuery, availableTerms}) => : '' ].join(' ')} onMouseEnter={setFocusedSuggestion.bind(null, index)} - onClick={addQueryTerm.bind(null, suggestion)} + onClick={finalizeInput.bind(null, suggestion)} title="Cliquez pour ajouter ce terme à la recherche" > {suggestion.name} @@ -206,7 +180,8 @@ const TermInput = ({query, setQuery, availableTerms}) => TermInput.propTypes = { query: PropTypes.arrayOf(Term).isRequired, - setQuery: PropTypes.func.isRequired, + addQueryTerm: PropTypes.func.isRequired, + removeQueryTerm: PropTypes.func.isRequired, availableTerms: PropTypes.arrayOf(Term).isRequired, }; diff --git a/app/src/util.js b/app/src/util.js index 9c831ad..1fb875e 100644 --- a/app/src/util.js +++ b/app/src/util.js @@ -32,9 +32,6 @@ export const useAsync = (initial, func, ...args) => * * — si la liste n’est pas vide, focus ∈ [0, taille de la liste]. * — sinon, focus = 0. - * - * @return Objet contenant la liste, l’indice de l’élément ayant le focus ainsi - * que des fonctions de modification. */ export const useFocusableList = () => { @@ -42,7 +39,14 @@ export const useFocusableList = () => const [focus, setFocus] = useState(0); return { + /** + * Contenu courant de la liste. + */ list, + + /** + * Indice de l’élément ayant présentement le focus. + */ focus, /** @@ -88,3 +92,47 @@ export const useFocusableList = () => } }; }; + +/** + * Crée une liste de termes de recherche. + */ +export const useQuery = () => +{ + const [query, setQuery] = useState([]); + + return { + /** + * Liste des termes de la requête. + */ + query, + + /** + * Ajoute un terme à la requête. + * + * @param term Terme à ajouter. + */ + addTerm(term) + { + if (query.some(({id}) => id === term.id)) + { + // N’ajoute pas les termes déjà sélectionnés + return; + } + + setQuery(query.concat([term])); + }, + + /** + * Supprime un terme de la requête. + * + * @param index Indice du terme à supprimer dans la liste. + */ + removeTerm(index) + { + setQuery([ + ...query.slice(0, index), + ...query.slice(index + 1) + ]); + }, + }; +};