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 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.
|
* Codes des touches du clavier par nom.
|
||||||
|
@ -13,41 +14,6 @@ const keys = Object.assign(Object.create(null), {
|
||||||
'down': 40,
|
'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.
|
* Zone de saisie des termes de recherche.
|
||||||
*
|
*
|
||||||
|
@ -62,10 +28,12 @@ const TermInput = ({terms, availableTerms, setTerms}) =>
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
|
|
||||||
// Liste des termes suggérés par autocomplétion
|
// Liste des termes suggérés par autocomplétion
|
||||||
const [suggestions, setSuggestions] = useState([]);
|
const {
|
||||||
|
list: suggestions,
|
||||||
// Terme ayant le focus parmi les suggestions
|
setList: setSuggestions,
|
||||||
const [focusedSuggestion, setFocusedSuggestion] = useState(0);
|
focus: focusedSuggestion,
|
||||||
|
setFocus: setFocusedSuggestion
|
||||||
|
} = util.useFocusableList();
|
||||||
|
|
||||||
// Référence au champ de saisie
|
// Référence au champ de saisie
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
@ -86,7 +54,6 @@ const TermInput = ({terms, availableTerms, setTerms}) =>
|
||||||
setTerms(terms.concat([term]));
|
setTerms(terms.concat([term]));
|
||||||
setValue('');
|
setValue('');
|
||||||
setSuggestions([]);
|
setSuggestions([]);
|
||||||
setFocusedSuggestion(0);
|
|
||||||
|
|
||||||
if (inputRef.current !== null)
|
if (inputRef.current !== null)
|
||||||
{
|
{
|
||||||
|
@ -135,16 +102,13 @@ const TermInput = ({terms, availableTerms, setTerms}) =>
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
setTerms(terms.slice(0, -1));
|
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
|
// Touche Haut : focus de la suggestion précédente
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
setFocusedSuggestion(focusedSuggestion - 1);
|
setFocusedSuggestion(focusedSuggestion - 1);
|
||||||
}
|
}
|
||||||
else if (
|
else if (ev.keyCode === keys.down)
|
||||||
ev.keyCode === keys.down
|
|
||||||
&& focusedSuggestion < suggestions.length - 1
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
// Touche Bas : focus de la suggestion suivante
|
// Touche Bas : focus de la suggestion suivante
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
@ -169,16 +133,17 @@ const TermInput = ({terms, availableTerms, setTerms}) =>
|
||||||
}
|
}
|
||||||
else
|
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
|
// Sélectionne les termes qui n’ont pas déjà été choisis
|
||||||
.filter(({id: suggestionId}) =>
|
availableTerms.filter(({id: suggestionId}) =>
|
||||||
!terms.some(({id: termId}) =>
|
!terms.some(({id: termId}) =>
|
||||||
termId === suggestionId
|
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;
|
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