app: Désuffixage des mots de la recherche
This commit is contained in:
parent
dc749885bd
commit
c2fb067c77
|
@ -1,5 +1,6 @@
|
|||
import React, {useState, useRef} from 'react';
|
||||
import * as diacritics from 'diacritics';
|
||||
import * as fuzzy from '../data/fuzzy.js';
|
||||
import * as util from '../util.js';
|
||||
|
||||
/**
|
||||
* Codes des touches du clavier par nom.
|
||||
|
@ -13,41 +14,6 @@ const keys = Object.assign(Object.create(null), {
|
|||
'down': 40,
|
||||
});
|
||||
|
||||
/**
|
||||
* Met le nom d’un terme sous forme normalisée pour la recherche approximative.
|
||||
*
|
||||
* Dans la forme normalisée, tous les accents sont ôtés et la casse est réduite
|
||||
* en minuscules.
|
||||
*
|
||||
* @param name Nom à normaliser.
|
||||
* @return Liste des mots du nom de terme normalisé.
|
||||
*/
|
||||
const normalizeName = name =>
|
||||
diacritics.remove(name.toLowerCase())
|
||||
.split(/\s+/g);
|
||||
|
||||
/**
|
||||
* Vérifie si un terme est similaire à un préfixe saisi.
|
||||
*
|
||||
* @param prefix Préfixe saisi.
|
||||
* @param term Terme à vérifier.
|
||||
* @return Vrai si le terme est considéré comme similaire au préfixe.
|
||||
*/
|
||||
const termMatchesPrefix = (prefix, {name, alias}) =>
|
||||
{
|
||||
const haystack = [name].concat(alias)
|
||||
.map(normalizeName)
|
||||
.reduce((prev, next) => prev.concat(next), []);
|
||||
|
||||
const needle = normalizeName(prefix);
|
||||
|
||||
return needle.every(item =>
|
||||
haystack.some(word =>
|
||||
word.startsWith(item)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Zone de saisie des termes de recherche.
|
||||
*
|
||||
|
@ -62,10 +28,12 @@ const TermInput = ({terms, availableTerms, setTerms}) =>
|
|||
const [value, setValue] = useState('');
|
||||
|
||||
// Liste des termes suggérés par autocomplétion
|
||||
const [suggestions, setSuggestions] = useState([]);
|
||||
|
||||
// Terme ayant le focus parmi les suggestions
|
||||
const [focusedSuggestion, setFocusedSuggestion] = useState(0);
|
||||
const {
|
||||
list: suggestions,
|
||||
setList: setSuggestions,
|
||||
focus: focusedSuggestion,
|
||||
setFocus: setFocusedSuggestion
|
||||
} = util.useFocusableList();
|
||||
|
||||
// Référence au champ de saisie
|
||||
const inputRef = useRef(null);
|
||||
|
@ -86,7 +54,6 @@ const TermInput = ({terms, availableTerms, setTerms}) =>
|
|||
setTerms(terms.concat([term]));
|
||||
setValue('');
|
||||
setSuggestions([]);
|
||||
setFocusedSuggestion(0);
|
||||
|
||||
if (inputRef.current !== null)
|
||||
{
|
||||
|
@ -135,16 +102,13 @@ const TermInput = ({terms, availableTerms, setTerms}) =>
|
|||
ev.preventDefault();
|
||||
setTerms(terms.slice(0, -1));
|
||||
}
|
||||
else if (ev.keyCode === keys.up && focusedSuggestion > 0)
|
||||
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
|
||||
&& focusedSuggestion < suggestions.length - 1
|
||||
)
|
||||
else if (ev.keyCode === keys.down)
|
||||
{
|
||||
// Touche Bas : focus de la suggestion suivante
|
||||
ev.preventDefault();
|
||||
|
@ -169,16 +133,17 @@ const TermInput = ({terms, availableTerms, setTerms}) =>
|
|||
}
|
||||
else
|
||||
{
|
||||
setSuggestions(availableTerms
|
||||
setSuggestions(fuzzy.filterTerms(
|
||||
// Filtre les termes correspondant à la saisie
|
||||
nextValue,
|
||||
|
||||
// Sélectionne les termes qui n’ont pas déjà été choisis
|
||||
.filter(({id: suggestionId}) =>
|
||||
availableTerms.filter(({id: suggestionId}) =>
|
||||
!terms.some(({id: termId}) =>
|
||||
termId === suggestionId
|
||||
)
|
||||
)
|
||||
// Filtre ceux dont le nom correspond à la saisie
|
||||
.filter(termMatchesPrefix.bind(null, nextValue))
|
||||
);
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
import * as diacritics from 'diacritics';
|
||||
|
||||
/**
|
||||
* Motifs de désuffixation des mots français.
|
||||
*/
|
||||
const stemPatterns = [
|
||||
[/aux$/, 'al'],
|
||||
[/(.)s$/, '$1'],
|
||||
[/(.)e$/, '$1'],
|
||||
];
|
||||
|
||||
/**
|
||||
* Motif de séparation des mots.
|
||||
*/
|
||||
const splitPattern = /[\s-]+/g;
|
||||
|
||||
/**
|
||||
* Désuffixe un mot français.
|
||||
*
|
||||
* @param word Mot originel.
|
||||
* @return Mot désuffixé.
|
||||
*/
|
||||
const stemWord = word =>
|
||||
{
|
||||
let result = word;
|
||||
|
||||
for (let [pattern, replacement] of stemPatterns)
|
||||
{
|
||||
result = result.replace(pattern, replacement);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Met le nom d’un terme sous forme normalisée pour la recherche approximative.
|
||||
*
|
||||
* Dans la forme normalisée, tous les accents sont ôtés, la casse est réduite
|
||||
* en minuscules et les mots sont désuffixés.
|
||||
*
|
||||
* @param name Nom à normaliser.
|
||||
* @return Liste des mots du nom de terme normalisé.
|
||||
*/
|
||||
const normalizeName = name =>
|
||||
diacritics.remove(name.toLowerCase())
|
||||
.split(splitPattern)
|
||||
.map(stemWord);
|
||||
|
||||
/**
|
||||
* Filtre une liste de termes pour ne garder que ceux qui sont similaires à une
|
||||
* saisie.
|
||||
*
|
||||
* @param search Contenu de la saisie.
|
||||
* @param term Terme à vérifier.
|
||||
* @return Ensemble des termes sélectionnés.
|
||||
*/
|
||||
export const filterTerms = (search, terms) =>
|
||||
{
|
||||
// Normalisation des mots recherchés
|
||||
const needle = normalizeName(search);
|
||||
|
||||
console.log(needle);
|
||||
|
||||
return terms.filter(term =>
|
||||
{
|
||||
// Normalisation du nom et des alias du terme
|
||||
const haystack = [term.name].concat(term.alias)
|
||||
.map(normalizeName)
|
||||
.reduce((prev, next) => prev.concat(next), []);
|
||||
|
||||
return needle.every(item =>
|
||||
haystack.some(word =>
|
||||
word.startsWith(item)
|
||||
)
|
||||
);
|
||||
});
|
||||
};
|
|
@ -24,3 +24,46 @@ export const useAsync = (initial, func, ...args) =>
|
|||
|
||||
return results;
|
||||
};
|
||||
|
||||
/**
|
||||
* Crée un état composé d’une liste et d’un élément ayant le focus dans cette
|
||||
* liste. À la modification de la liste ou de l’indice de l’élément ayant le
|
||||
* focus, la contrainte focus ∈ [0, taille de la liste] est imposée.
|
||||
*
|
||||
* @return Objet contenant la liste, l’indice de l’élément ayant le focus ainsi
|
||||
* que des fonctions de modification.
|
||||
*/
|
||||
export const useFocusableList = () =>
|
||||
{
|
||||
const [list, setList] = useState([]);
|
||||
const [focus, setFocus] = useState(0);
|
||||
|
||||
return {
|
||||
list,
|
||||
focus,
|
||||
|
||||
/**
|
||||
* Modifie la liste et s’assure que l’élément ayant le focus est
|
||||
* toujours dans sa plage de valeurs possibles.
|
||||
*
|
||||
* @param nextList Nouvelle liste.
|
||||
*/
|
||||
setList(nextList)
|
||||
{
|
||||
setList(nextList);
|
||||
setFocus(Math.min(nextList.length, focus));
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Modifie l’élément ayant le focus en imposant les contraintes
|
||||
* d’intégrité.
|
||||
*
|
||||
* @param nextFocus Indice du nouvel élément ayant le focus.
|
||||
*/
|
||||
setFocus(nextFocus)
|
||||
{
|
||||
setFocus(Math.min(nextList.length, Math.max(0, focus)));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue