@forthtilliath/react-native-kit 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -1
- package/dist/confirmDestructive.d.ts +7 -0
- package/dist/confirmDestructive.d.ts.map +1 -0
- package/dist/confirmDestructive.js +13 -0
- package/dist/useDebouncedChange.d.ts +2 -0
- package/dist/useDebouncedChange.d.ts.map +1 -0
- package/dist/useDebouncedChange.js +24 -0
- package/dist/useSubmitGuard.d.ts +5 -0
- package/dist/useSubmitGuard.d.ts.map +1 -0
- package/dist/useSubmitGuard.js +19 -0
- package/dist/utils/escapeCsvField.d.ts +2 -0
- package/dist/utils/escapeCsvField.d.ts.map +1 -0
- package/dist/utils/escapeCsvField.js +8 -0
- package/dist/utils/escapeHtml.d.ts +2 -0
- package/dist/utils/escapeHtml.d.ts.map +1 -0
- package/dist/utils/escapeHtml.js +9 -0
- package/dist/utils/formatCsvNumber.d.ts +2 -0
- package/dist/utils/formatCsvNumber.d.ts.map +1 -0
- package/dist/utils/formatCsvNumber.js +6 -0
- package/dist/utils/getMostRecentIds.d.ts +6 -0
- package/dist/utils/getMostRecentIds.d.ts.map +1 -0
- package/dist/utils/getMostRecentIds.js +19 -0
- package/dist/utils/getPeriodStartMs.d.ts +3 -0
- package/dist/utils/getPeriodStartMs.d.ts.map +1 -0
- package/dist/utils/getPeriodStartMs.js +14 -0
- package/dist/utils/nextInCycle.d.ts +2 -0
- package/dist/utils/nextInCycle.d.ts.map +1 -0
- package/dist/utils/nextInCycle.js +9 -0
- package/dist/utils/normalizeForSearch.d.ts +2 -0
- package/dist/utils/normalizeForSearch.d.ts.map +1 -0
- package/dist/utils/normalizeForSearch.js +15 -0
- package/dist/utils/rankByNameMatch.d.ts +2 -0
- package/dist/utils/rankByNameMatch.d.ts.map +1 -0
- package/dist/utils/rankByNameMatch.js +33 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @forthtilliath/react-native-kit
|
|
2
2
|
|
|
3
|
-
Small
|
|
3
|
+
Small React Native building blocks — components, hooks, and framework-agnostic utils — with no opinion on your app's theme, distributed one file per export so consumers only pull in what they use.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -70,6 +70,79 @@ The component ships with neutral default styles and no opinion on your app's the
|
|
|
70
70
|
|
|
71
71
|
Every field of `styles` is optional — pass only the ones you want to override; the rest fall back to the defaults (`ChangelogNotesStyles` in `ChangelogNotes.tsx`).
|
|
72
72
|
|
|
73
|
+
### `useSubmitGuard()`
|
|
74
|
+
|
|
75
|
+
Prevents a second call while a first one is still pending — e.g. a double-tap on a "Save" button before it's had time to disable, which would otherwise create duplicate submissions.
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { useSubmitGuard } from "@forthtilliath/react-native-kit/useSubmitGuard";
|
|
79
|
+
|
|
80
|
+
const { isSaving, guard } = useSubmitGuard();
|
|
81
|
+
|
|
82
|
+
<Pressable
|
|
83
|
+
disabled={isSaving}
|
|
84
|
+
onPress={() =>
|
|
85
|
+
guard(async () => {
|
|
86
|
+
await save();
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
>
|
|
90
|
+
<Text>{isSaving ? "Saving…" : "Save"}</Text>
|
|
91
|
+
</Pressable>;
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### `useDebouncedChange(values, delayMs, callback)`
|
|
95
|
+
|
|
96
|
+
Calls `callback` `delayMs` after the last change among `values`, ignoring renders where any value is still `undefined` (not loaded yet) and the very first render where they're all defined (no trigger on mount). A new change before the delay elapses resets the timer — a real debounce, not a throttle.
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import { useDebouncedChange } from "@forthtilliath/react-native-kit/useDebouncedChange";
|
|
100
|
+
|
|
101
|
+
useDebouncedChange([settingsData, itemsData], 5 * 60 * 1000, () => {
|
|
102
|
+
runAutoBackup();
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `confirmDestructive(title, onConfirm, options?)`
|
|
107
|
+
|
|
108
|
+
Generic destructive-action confirmation (title + message + Cancel/Confirm), for anything irreversible (delete, reset...). Ships with French defaults (`message`, `cancelLabel`, `confirmLabel` all overridable).
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { confirmDestructive } from "@forthtilliath/react-native-kit/confirmDestructive";
|
|
112
|
+
|
|
113
|
+
confirmDestructive("Delete this item?", () => deleteItem(id), {
|
|
114
|
+
message: "This cannot be undone.",
|
|
115
|
+
cancelLabel: "Cancel",
|
|
116
|
+
confirmLabel: "Delete",
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Utils (`utils/`)
|
|
121
|
+
|
|
122
|
+
Framework-agnostic pure functions — no React or React Native import, usable from Node/web too.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { getPeriodStartMs } from "@forthtilliath/react-native-kit/utils/getPeriodStartMs";
|
|
126
|
+
import { getMostRecentIds } from "@forthtilliath/react-native-kit/utils/getMostRecentIds";
|
|
127
|
+
import { nextInCycle } from "@forthtilliath/react-native-kit/utils/nextInCycle";
|
|
128
|
+
import { normalizeForSearch } from "@forthtilliath/react-native-kit/utils/normalizeForSearch";
|
|
129
|
+
import { rankByNameMatch } from "@forthtilliath/react-native-kit/utils/rankByNameMatch";
|
|
130
|
+
import { escapeCsvField } from "@forthtilliath/react-native-kit/utils/escapeCsvField";
|
|
131
|
+
import { formatCsvNumber } from "@forthtilliath/react-native-kit/utils/formatCsvNumber";
|
|
132
|
+
import { escapeHtml } from "@forthtilliath/react-native-kit/utils/escapeHtml";
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
| Function | What it does |
|
|
136
|
+
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
137
|
+
| `getPeriodStartMs(period, now?)` | Start timestamp (ms) for a `"today" \| "7d" \| "30d" \| "all"` period filter, or `null` for `"all"`. `"today"` is the current calendar day, not a rolling 24h. |
|
|
138
|
+
| `getMostRecentIds(rows, limit?)` | Most recently occurring distinct ids (`{ id, occurredAt }[]`), most recent first, deduplicated, limited (default 5). |
|
|
139
|
+
| `nextInCycle(ids, currentId)` | Next id in a short list (e.g. cycling through recents on tap) — wraps to the first if `currentId` is at the end or no longer in the list. |
|
|
140
|
+
| `normalizeForSearch(text)` | Lowercases, trims, strips accents, and expands `œ`/`æ` ligatures (which `normalize("NFD")` alone doesn't decompose) — for accent/case-insensitive search matching. |
|
|
141
|
+
| `rankByNameMatch(items, query, getName)` | Ranks `items` by relevance to `query`: earlier match position first, then shorter name — for a search-as-you-type list. |
|
|
142
|
+
| `escapeCsvField(value)` | Quotes and escapes a CSV field (RFC 4180) only if it contains a `"`, `;`, or newline. |
|
|
143
|
+
| `formatCsvNumber(value, decimals?)` | Formats a number with a comma decimal separator (French-locale spreadsheets) instead of JS's dot. |
|
|
144
|
+
| `escapeHtml(text)` | Basic HTML entity escaping (`&`, `<`, `>`, `"`) for inserting user text into an HTML template. |
|
|
145
|
+
|
|
73
146
|
## Scripts
|
|
74
147
|
|
|
75
148
|
```bash
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface ConfirmDestructiveOptions {
|
|
2
|
+
message?: string;
|
|
3
|
+
cancelLabel?: string;
|
|
4
|
+
confirmLabel?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function confirmDestructive(title: string, onConfirm: () => void, options?: ConfirmDestructiveOptions): void;
|
|
7
|
+
//# sourceMappingURL=confirmDestructive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"confirmDestructive.d.ts","sourceRoot":"","sources":["../src/confirmDestructive.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAQD,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,IAAI,EACrB,OAAO,GAAE,yBAA8B,QAWxC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Alert } from "react-native";
|
|
2
|
+
const DEFAULT_MESSAGE = "Cette action est définitive.";
|
|
3
|
+
const DEFAULT_CANCEL_LABEL = "Annuler";
|
|
4
|
+
const DEFAULT_CONFIRM_LABEL = "Supprimer";
|
|
5
|
+
// Confirmation destructive générique (titre + message + Annuler/Confirmer),
|
|
6
|
+
// pour toute action irréversible (suppression, réinitialisation...).
|
|
7
|
+
export function confirmDestructive(title, onConfirm, options = {}) {
|
|
8
|
+
const { message = DEFAULT_MESSAGE, cancelLabel = DEFAULT_CANCEL_LABEL, confirmLabel = DEFAULT_CONFIRM_LABEL, } = options;
|
|
9
|
+
Alert.alert(title, message, [
|
|
10
|
+
{ text: cancelLabel, style: "cancel" },
|
|
11
|
+
{ text: confirmLabel, style: "destructive", onPress: onConfirm },
|
|
12
|
+
]);
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useDebouncedChange.d.ts","sourceRoot":"","sources":["../src/useDebouncedChange.ts"],"names":[],"mappings":"AASA,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,OAAO,EAAE,EACjB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,IAAI,QAkBrB"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
// Déclenche `callback` `delayMs` après le dernier changement parmi `values`,
|
|
3
|
+
// en ignorant tout rendu tant qu'au moins une valeur vaut `undefined` (ex: une
|
|
4
|
+
// donnée pas encore chargée) et le tout premier rendu où elles le sont toutes
|
|
5
|
+
// (pour éviter un déclenchement au montage). Le délai est annulé si une
|
|
6
|
+
// nouvelle valeur arrive avant qu'il n'expire (vrai debounce). `values` doit
|
|
7
|
+
// avoir la même longueur à chaque rendu, comme pour un tableau de
|
|
8
|
+
// dépendances de useEffect classique.
|
|
9
|
+
export function useDebouncedChange(values, delayMs, callback) {
|
|
10
|
+
const hasInitializedRef = useRef(false);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (values.some((value) => value === undefined))
|
|
13
|
+
return;
|
|
14
|
+
if (!hasInitializedRef.current) {
|
|
15
|
+
hasInitializedRef.current = true;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const timeout = setTimeout(callback, delayMs);
|
|
19
|
+
return () => {
|
|
20
|
+
clearTimeout(timeout);
|
|
21
|
+
};
|
|
22
|
+
// eslint-disable-next-line @eslint-react/exhaustive-deps, react-hooks/exhaustive-deps -- `values` IS the dependency list, by design (caller-provided, variable length not expected to change).
|
|
23
|
+
}, values);
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSubmitGuard.d.ts","sourceRoot":"","sources":["../src/useSubmitGuard.ts"],"names":[],"mappings":"AAKA,wBAAgB,cAAc;;oBAGC,MAAM,OAAO,CAAC,IAAI,CAAC;EAWjD"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
// Empêche un second appel pendant qu'un premier est encore en cours (ex:
|
|
3
|
+
// double-tap sur un bouton "Enregistrer" avant qu'il n'ait eu le temps de se
|
|
4
|
+
// désactiver, qui créerait plusieurs soumissions identiques).
|
|
5
|
+
export function useSubmitGuard() {
|
|
6
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
7
|
+
async function guard(action) {
|
|
8
|
+
if (isSaving)
|
|
9
|
+
return;
|
|
10
|
+
setIsSaving(true);
|
|
11
|
+
try {
|
|
12
|
+
await action();
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
setIsSaving(false);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return { isSaving, guard };
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escapeCsvField.d.ts","sourceRoot":"","sources":["../../src/utils/escapeCsvField.ts"],"names":[],"mappings":"AAGA,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGpD"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Échappe un champ CSV (RFC 4180) : entoure de guillemets et double les
|
|
2
|
+
// guillemets internes si la valeur contient un guillemet, un point-virgule
|
|
3
|
+
// (délimiteur) ou un saut de ligne — sinon la valeur est renvoyée telle quelle.
|
|
4
|
+
export function escapeCsvField(value) {
|
|
5
|
+
if (!/[";\n]/.test(value))
|
|
6
|
+
return value;
|
|
7
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
8
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escapeHtml.d.ts","sourceRoot":"","sources":["../../src/utils/escapeHtml.ts"],"names":[],"mappings":"AAEA,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM/C"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Échappement HTML basique (esperluette, chevrons, guillemets) pour insérer
|
|
2
|
+
// du texte utilisateur dans un gabarit HTML sans dépendance de templating.
|
|
3
|
+
export function escapeHtml(text) {
|
|
4
|
+
return text
|
|
5
|
+
.replace(/&/g, "&")
|
|
6
|
+
.replace(/</g, "<")
|
|
7
|
+
.replace(/>/g, ">")
|
|
8
|
+
.replace(/"/g, """);
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formatCsvNumber.d.ts","sourceRoot":"","sources":["../../src/utils/formatCsvNumber.ts"],"names":[],"mappings":"AAGA,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,MAAM,CAEnE"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Nombre brut (pas de suffixe d'unité) pour rester exploitable dans un
|
|
2
|
+
// tableur (somme, moyenne...), avec la virgule décimale attendue par Excel
|
|
3
|
+
// en locale française plutôt que le point de JS.
|
|
4
|
+
export function formatCsvNumber(value, decimals = 1) {
|
|
5
|
+
return value.toFixed(decimals).replace(".", ",");
|
|
6
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getMostRecentIds.d.ts","sourceRoot":"","sources":["../../src/utils/getMostRecentIds.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAQD,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,WAAW,EAAE,EACnB,KAAK,SAAgB,GACpB,MAAM,EAAE,CAcV"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const DEFAULT_LIMIT = 5;
|
|
2
|
+
// Ids distincts les plus récents, du plus récent au plus ancien — utile pour
|
|
3
|
+
// un sélecteur "récents" (aliment, récipient, ou tout autre entité datée).
|
|
4
|
+
// `id` peut être null (référence supprimée depuis) : ignoré, on ne peut plus
|
|
5
|
+
// le proposer.
|
|
6
|
+
export function getMostRecentIds(rows, limit = DEFAULT_LIMIT) {
|
|
7
|
+
const sorted = [...rows].sort((a, b) => new Date(b.occurredAt).getTime() - new Date(a.occurredAt).getTime());
|
|
8
|
+
const seen = new Set();
|
|
9
|
+
const result = [];
|
|
10
|
+
for (const row of sorted) {
|
|
11
|
+
if (row.id == null || seen.has(row.id))
|
|
12
|
+
continue;
|
|
13
|
+
seen.add(row.id);
|
|
14
|
+
result.push(row.id);
|
|
15
|
+
if (result.length >= limit)
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getPeriodStartMs.d.ts","sourceRoot":"","sources":["../../src/utils/getPeriodStartMs.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,OAAO,GAAG,IAAI,GAAG,KAAK,CAAC;AAK1D,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,YAAY,EACpB,GAAG,OAAa,GACf,MAAM,GAAG,IAAI,CASf"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Timestamp (ms) à partir duquel un enregistrement est inclus pour cette
|
|
2
|
+
// période, ou null pour "all" (pas de filtre de date). "today" part du début
|
|
3
|
+
// du jour calendaire courant, pas d'il y a 24h glissantes.
|
|
4
|
+
export function getPeriodStartMs(period, now = new Date()) {
|
|
5
|
+
if (period === "all")
|
|
6
|
+
return null;
|
|
7
|
+
if (period === "today") {
|
|
8
|
+
const startOfDay = new Date(now);
|
|
9
|
+
startOfDay.setHours(0, 0, 0, 0);
|
|
10
|
+
return startOfDay.getTime();
|
|
11
|
+
}
|
|
12
|
+
const days = period === "7d" ? 7 : 30;
|
|
13
|
+
return now.getTime() - days * 24 * 60 * 60 * 1000;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nextInCycle.d.ts","sourceRoot":"","sources":["../../src/utils/nextInCycle.ts"],"names":[],"mappings":"AAGA,wBAAgB,WAAW,CACzB,GAAG,EAAE,MAAM,EAAE,EACb,SAAS,EAAE,MAAM,GAAG,IAAI,GACvB,MAAM,GAAG,IAAI,CAIf"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Élément suivant dans une liste courte (ex: cycler sur tap entre quelques
|
|
2
|
+
// éléments récents). Reprend au début si l'actuel n'y est plus (supprimé, ou
|
|
3
|
+
// hors de la liste) ou si on est en bout de liste.
|
|
4
|
+
export function nextInCycle(ids, currentId) {
|
|
5
|
+
if (ids.length === 0)
|
|
6
|
+
return null;
|
|
7
|
+
const currentIndex = currentId == null ? -1 : ids.indexOf(currentId);
|
|
8
|
+
return ids[(currentIndex + 1) % ids.length] ?? null;
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalizeForSearch.d.ts","sourceRoot":"","sources":["../../src/utils/normalizeForSearch.ts"],"names":[],"mappings":"AASA,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMvD"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Les ligatures œ/æ ne se décomposent PAS via normalize("NFD") (contrairement
|
|
2
|
+
// aux lettres accentuées) : ce sont des lettres Unicode à part entière, pas
|
|
3
|
+
// une base + un signe combinant. Sans ce remplacement explicite, chercher
|
|
4
|
+
// "Œuf" (avec la vraie ligature) ne trouve jamais "Oeuf" dans une base qui
|
|
5
|
+
// l'orthographie sans ligature (comme la plupart des bases de données texte).
|
|
6
|
+
function expandLigatures(text) {
|
|
7
|
+
return text.replace(/[œŒ]/g, "oe").replace(/[æÆ]/g, "ae");
|
|
8
|
+
}
|
|
9
|
+
export function normalizeForSearch(text) {
|
|
10
|
+
return expandLigatures(text)
|
|
11
|
+
.normalize("NFD")
|
|
12
|
+
.replace(/[̀-ͯ]/g, "")
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.trim();
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rankByNameMatch.d.ts","sourceRoot":"","sources":["../../src/utils/rankByNameMatch.ts"],"names":[],"mappings":"AAsBA,wBAAgB,eAAe,CAAC,CAAC,EAC/B,KAAK,EAAE,CAAC,EAAE,EACV,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAC3B,CAAC,EAAE,CAiBL"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { normalizeForSearch } from "./normalizeForSearch.js";
|
|
2
|
+
// Cache le nom normalisé de chaque élément par référence d'objet : utile pour
|
|
3
|
+
// des listes figées (constantes de module) cherchées à chaque frappe clavier
|
|
4
|
+
// dans un champ de recherche, pour éviter de renormaliser tous les noms à
|
|
5
|
+
// chaque appel.
|
|
6
|
+
const normalizedNameCache = new WeakMap();
|
|
7
|
+
function getNormalizedName(item, getName) {
|
|
8
|
+
if (typeof item !== "object" || item === null)
|
|
9
|
+
return normalizeForSearch(getName(item));
|
|
10
|
+
const cached = normalizedNameCache.get(item);
|
|
11
|
+
if (cached !== undefined)
|
|
12
|
+
return cached;
|
|
13
|
+
const normalized = normalizeForSearch(getName(item));
|
|
14
|
+
normalizedNameCache.set(item, normalized);
|
|
15
|
+
return normalized;
|
|
16
|
+
}
|
|
17
|
+
// Classe des éléments par pertinence vis-à-vis d'une recherche texte :
|
|
18
|
+
// position du match dans le nom (plus tôt = plus pertinent), puis longueur
|
|
19
|
+
// du nom. Générique pour être réutilisable par n'importe quelle liste
|
|
20
|
+
// d'éléments ayant un nom affichable.
|
|
21
|
+
export function rankByNameMatch(items, query, getName) {
|
|
22
|
+
const normalizedQuery = normalizeForSearch(query);
|
|
23
|
+
const matches = [];
|
|
24
|
+
for (const item of items) {
|
|
25
|
+
const matchIndex = getNormalizedName(item, getName).indexOf(normalizedQuery);
|
|
26
|
+
if (matchIndex !== -1) {
|
|
27
|
+
matches.push({ item, matchIndex });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
matches.sort((a, b) => a.matchIndex - b.matchIndex ||
|
|
31
|
+
getName(a.item).length - getName(b.item).length);
|
|
32
|
+
return matches.map((m) => m.item);
|
|
33
|
+
}
|