@zevra/ui 0.43.1 → 0.44.1

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/dist/grille.js CHANGED
@@ -27,6 +27,7 @@ import { SelectMenu } from './select-menu.js';
27
27
  import { Menu, MenuItem, Pager, PagerArrow, Popover } from './navigation.js';
28
28
  import { Empty } from './feedback.js';
29
29
  import { Meta } from './data.js';
30
+ import { Mono } from './typography.js';
30
31
  import { cx } from './cx.js';
31
32
  export { usePersistedState };
32
33
  /** Identifiant réservé de la colonne de cases. */
@@ -46,6 +47,11 @@ export const TEXTES_GRILLE = {
46
47
  min: 'Min.',
47
48
  max: 'Max.',
48
49
  effacer: 'Effacer le filtre',
50
+ effacerFiltres: 'Effacer les filtres',
51
+ toutAfficher: 'Tout afficher',
52
+ aucun: 'Aucun',
53
+ rechercherValeur: 'Rechercher une valeur',
54
+ aucuneValeur: 'Aucune valeur.',
49
55
  periodes: 'Périodes',
50
56
  aujourdhui: "Aujourd'hui",
51
57
  septJours: '7 derniers jours',
@@ -192,15 +198,22 @@ const nombre = (s) => {
192
198
  const n = Number(s.replace(',', '.'));
193
199
  return Number.isFinite(n) ? n : null;
194
200
  };
201
+ /** Sans casse ni accents : « decision » trouve « Décision ». */
202
+ export function normaliser(v) {
203
+ return String(v ?? '')
204
+ .toLowerCase()
205
+ .normalize('NFD')
206
+ .replace(/[\u0300-\u036f]/g, '');
207
+ }
195
208
  /** Le filtre de toutes les colonnes de la grille : il lit la FORME de la
196
209
  * valeur, pas la colonne. Bornes comprises. */
197
210
  export const filtreGrille = (ligne, colonne, filtre) => {
198
211
  const brut = ligne.getValue(colonne);
199
212
  switch (filtre?.type) {
200
213
  case 'texte':
201
- return String(brut ?? '').toLowerCase().includes(filtre.valeur.trim().toLowerCase());
214
+ return normaliser(brut).includes(normaliser(filtre.valeur.trim()));
202
215
  case 'choix':
203
- return String(brut ?? '') === filtre.valeur;
216
+ return filtre.valeurs.includes(String(brut ?? ''));
204
217
  case 'nombre': {
205
218
  const v = typeof brut === 'number' ? brut : nombre(String(brut ?? ''));
206
219
  const min = nombre(filtre.min);
@@ -221,7 +234,7 @@ export const filtreGrille = (ligne, colonne, filtre) => {
221
234
  };
222
235
  filtreGrille.autoRemove = (f) => !f ||
223
236
  (f.type === 'texte' && f.valeur.trim() === '') ||
224
- (f.type === 'choix' && f.valeur === '') ||
237
+ (f.type === 'choix' && f.valeurs.length === 0) ||
225
238
  (f.type === 'nombre' && f.min === '' && f.max === '') ||
226
239
  (f.type === 'date' && f.du === '' && f.au === '');
227
240
  /** Ne garde d'une saisie que ce qui peut faire un nombre : chiffres, un
@@ -394,21 +407,22 @@ function PoigneeLargeur({ colonne, table, nom, t, }) {
394
407
  // une valeur, et les flèches pour la changer.
395
408
  role: "separator", "aria-orientation": "vertical", "aria-label": t.largeur(nom), "aria-valuenow": Math.round(reglee ?? colonne.getSize()), "aria-valuemin": min, tabIndex: 0, title: t.largeurAide, className: cx('zv-colhead__resize', actif && 'zv-colhead__resize--actif'), onPointerDown: appui, onPointerMove: mouvement, onPointerUp: fin, onPointerCancel: fin, onDoubleClick: rendre, onKeyDown: clavier, onClick: (e) => e.stopPropagation() }));
396
409
  }
397
- /* ─── Cases de sélection ─────────────────────────────────────────────── */
398
- function CaseTout({ table, t }) {
410
+ function CaseTout({ table, t, options }) {
399
411
  // Une enveloppe plutôt qu'une ref sur la case : `Checkbox` ne transmet
400
412
  // pas de ref sous React 18, que le paquet accepte encore.
401
413
  const ref = useRef(null);
402
414
  const partiel = table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected();
403
415
  // `indeterminate` n'a pas d'attribut HTML : seule la propriété DOM existe.
416
+ // Sans elle, « 3 sur 40 cochées » ressemble à « aucune ».
404
417
  useEffect(() => {
405
418
  const caseTout = ref.current?.querySelector('input');
406
419
  if (caseTout)
407
420
  caseTout.indeterminate = partiel;
408
421
  }, [partiel]);
409
- return (_jsx("span", { ref: ref, className: "zv-cell--case", children: _jsx(Checkbox, { checked: table.getIsAllRowsSelected(), onChange: table.getToggleAllRowsSelectedHandler(), "aria-label": t.toutSelectionner }) }));
422
+ const aucune = !table.getPrePaginationRowModel().rows.some((r) => r.getCanSelect());
423
+ return (_jsx("span", { ref: ref, className: "zv-cell--case", children: _jsx(Checkbox, { checked: table.getIsAllRowsSelected(), disabled: aucune || options.current?.desactivee, onChange: table.getToggleAllRowsSelectedHandler(), "aria-label": t.toutSelectionner, title: t.toutSelectionner }) }));
410
424
  }
411
- function colonneSelection(t) {
425
+ function colonneSelection(t, options) {
412
426
  return {
413
427
  id: COLONNE_SELECTION,
414
428
  size: 40,
@@ -419,11 +433,13 @@ function colonneSelection(t) {
419
433
  enableColumnFilter: false,
420
434
  enableGlobalFilter: false,
421
435
  enablePinning: false,
422
- header: ({ table }) => _jsx(CaseTout, { table: table, t: t }),
423
- cell: ({ row }) => (_jsx(Checkbox, { checked: row.getIsSelected(), disabled: !row.getCanSelect(), onChange: row.getToggleSelectedHandler(),
424
- // La case vit dans une ligne cliquable : sans ces deux arrêts, cocher
425
- // ouvrirait aussi la ligne, et Espace l'ouvrirait au clavier.
426
- onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), "aria-label": t.selectionnerLigne })),
436
+ header: ({ table }) => _jsx(CaseTout, { table: table, t: t, options: options }),
437
+ // Une ligne qui ne se sélectionne pas n'a PAS de case : une case grisée
438
+ // promettrait une action qui n'existe pas pour elle.
439
+ cell: ({ row }) => row.getCanSelect() ? (_jsx(Checkbox, { checked: row.getIsSelected(), disabled: options.current?.desactivee, onChange: row.getToggleSelectedHandler(),
440
+ // La case vit dans une ligne cliquable : sans ces deux arrêts,
441
+ // cocher ouvrirait aussi la ligne, et Espace l'ouvrirait au clavier.
442
+ onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), "aria-label": options.current?.label?.(row.original) ?? t.selectionnerLigne })) : null,
427
443
  meta: { classe: 'zv-cell--case' },
428
444
  };
429
445
  }
@@ -455,16 +471,16 @@ function MenuColonnes({ table, t }) {
455
471
  };
456
472
  }, [ouvert]);
457
473
  const colonnes = table.getAllLeafColumns().filter((c) => c.id !== COLONNE_SELECTION);
458
- return (_jsx("div", { ref: ancre, children: _jsxs(SelectAnchor, { children: [_jsx(Button, { variante: "fantome", compact: true, icone: "reglages", "aria-expanded": ouvert, "aria-haspopup": "true", onClick: () => setOuvert((o) => !o), children: t.colonnes }), ouvert && (_jsx(Menu, { role: "group", "aria-label": t.colonnesAffichees, className: "zv-grille__menu", children: colonnes.map((c) => {
459
- const nom = libelleColonne(c);
460
- const cote = c.getIsPinned();
461
- const suivant = cote === false ? 'left' : cote === 'left' ? 'right' : false;
462
- return (_jsxs("div", { className: "zv-grille__menu-ligne", children: [_jsxs("label", { className: "zv-menu__item zv-grille__menu-case", children: [_jsx(Checkbox, { checked: c.getIsVisible(), disabled: !c.getCanHide(), onChange: c.getToggleVisibilityHandler() }), _jsx("span", { children: nom }), cote && (_jsx(Meta, { taille: "sm", children: cote === 'left' ? t.aGauche : t.aDroite }))] }), c.getCanPin() && (_jsx(IconButton, { icone: "epingle", taille: "sm", className: "zv-grille__epingle", "aria-pressed": cote !== false, label: suivant === 'left'
463
- ? t.epinglerGauche(nom)
464
- : suivant === 'right'
465
- ? t.epinglerDroite(nom)
466
- : t.detacher(nom), onClick: () => c.pin(suivant) }))] }, c.id));
467
- }) }))] }) }));
474
+ return (_jsx("div", { ref: ancre, children: _jsxs(SelectAnchor, { children: [_jsx(Button, { variante: "fantome", compact: true, icone: "reglages", "aria-expanded": ouvert, "aria-haspopup": "true", onClick: () => setOuvert((o) => !o), children: t.colonnes }), ouvert && (_jsxs(Menu, { role: "group", "aria-label": t.colonnesAffichees, className: "zv-grille__menu", children: [colonnes.some((c) => !c.getIsVisible()) && (_jsx(Button, { variante: "fantome", compact: true, onClick: () => table.setColumnVisibility((v) => Object.fromEntries(Object.keys(v).map((k) => [k, true]))), children: t.toutAfficher })), colonnes.map((c) => {
475
+ const nom = libelleColonne(c);
476
+ const cote = c.getIsPinned();
477
+ const suivant = cote === false ? 'left' : cote === 'left' ? 'right' : false;
478
+ return (_jsxs("div", { className: "zv-grille__menu-ligne", children: [_jsxs("label", { className: "zv-menu__item zv-grille__menu-case", children: [_jsx(Checkbox, { checked: c.getIsVisible(), disabled: !c.getCanHide(), onChange: c.getToggleVisibilityHandler() }), _jsx("span", { children: nom }), cote && (_jsx(Meta, { taille: "sm", children: cote === 'left' ? t.aGauche : t.aDroite }))] }), c.getCanPin() && (_jsx(IconButton, { icone: "epingle", taille: "sm", className: "zv-grille__epingle", "aria-pressed": cote !== false, label: suivant === 'left'
479
+ ? t.epinglerGauche(nom)
480
+ : suivant === 'right'
481
+ ? t.epinglerDroite(nom)
482
+ : t.detacher(nom), onClick: () => c.pin(suivant) }))] }, c.id));
483
+ })] }))] }) }));
468
484
  }
469
485
  /* ─── Filtres de colonne ────────────────────────────────────────────── */
470
486
  function FiltreColonne({ colonne, type, t, }) {
@@ -563,35 +579,35 @@ function FiltreFlottant({ libelle, resume, vide, icone, role, panneau: contenu,
563
579
  return (_jsxs(_Fragment, { children: [_jsxs("button", { ref: declencheur, type: "button", className: "zv-input zv-input--trigger zv-table__filtre", "data-vide": vide ? 'oui' : undefined, "aria-expanded": ouvert, "aria-haspopup": role, "aria-label": `${libelle} : ${resume}`, onClick: () => setOuvert((o) => !o), children: [_jsx("span", { className: "zv-colhead__label", children: resume }), _jsx(Icon, { nom: icone, taille: "sm", discret: true })] }), ouvert &&
564
580
  createPortal(_jsx("div", { ref: panneau, className: "zv-grille__flottant", style: position ? { top: position.top, left: position.left } : { visibility: 'hidden' }, children: contenu(fermer) }), declencheur.current?.ownerDocument.body ?? document.body)] }));
565
581
  }
566
- /** Les flèches promènent le focus d'une option à l'autre, Origine et Fin
567
- * vont aux extrémités : le parcours clavier d'une liste, que le <select>
568
- * natif donnait et qu'une liste dessinée doit rendre. */
569
- function parcourirListe(e) {
570
- const options = [...e.currentTarget.querySelectorAll('[role="option"]')];
571
- const i = options.indexOf(e.target);
572
- const vers = e.key === 'ArrowDown' ? i + 1 : e.key === 'ArrowUp' ? i - 1 : e.key === 'Home' ? 0 : e.key === 'End' ? options.length - 1 : null;
573
- if (vers === null)
574
- return;
575
- e.preventDefault();
576
- options[Math.max(0, Math.min(options.length - 1, vers))]?.focus();
577
- }
578
582
  function FiltreChoix({ colonne, nom, t }) {
579
583
  const filtre = colonne.getFilterValue();
580
- // Les valeurs du modèle FACETTÉ : celles que les autres filtres laissent
581
- // encore passer. Une option qui ne rendrait aucune ligne n'est pas offerte.
582
- const options = [...colonne.getFacetedUniqueValues().keys()]
584
+ const choisies = filtre?.type === 'choix' ? filtre.valeurs : [];
585
+ // Les valeurs du modèle FACETTÉ, avec leur nombre de lignes : celles que
586
+ // les autres filtres laissent encore passer. Une option qui ne rendrait
587
+ // aucune ligne n'est pas offerte, sauf si elle est déjà cochée : sinon
588
+ // la liste mentirait sur ce qui filtre.
589
+ const comptes = colonne.getFacetedUniqueValues();
590
+ const options = [...comptes.keys()]
583
591
  .filter((v) => v != null && v !== '')
584
592
  .map(String)
585
- .sort((a, b) => a.localeCompare(b, 'fr'));
586
- const valeur = filtre?.type === 'choix' ? filtre.valeur : '';
587
- // L'option choisie reste dans la liste même si un autre filtre l'a fait
588
- // disparaître : sinon la liste afficherait « Tous » en mentant.
589
- if (valeur && !options.includes(valeur))
590
- options.unshift(valeur);
591
- return (_jsx(FiltreFlottant, { libelle: t.filtrerColonne(nom), resume: valeur || t.tous, vide: !valeur, icone: "chevron-bas", role: "listbox", panneau: (fermer) => (_jsx(Menu, { role: "listbox", "aria-label": t.filtrerColonne(nom), className: "zv-grille__liste", onKeyDown: parcourirListe, children: ['', ...options].map((o) => (_jsx(MenuItem, { actif: o === valeur, onClick: () => {
592
- colonne.setFilterValue({ type: 'choix', valeur: o });
593
- fermer();
594
- }, children: o || t.tous }, o || '\u0000'))) })) }));
593
+ .sort((a, b) => a.localeCompare(b, 'fr', { numeric: true }));
594
+ for (const c of choisies)
595
+ if (!options.includes(c))
596
+ options.unshift(c);
597
+ const ecrire = (valeurs) => colonne.setFilterValue({ type: 'choix', valeurs });
598
+ const resume = choisies.length === 0 ? t.tous : choisies.length === 1 ? (choisies[0] ?? t.tous) : `${choisies.length} / ${options.length}`;
599
+ return (_jsx(FiltreFlottant, { libelle: t.filtrerColonne(nom), resume: resume, vide: choisies.length === 0, icone: "chevron-bas", role: "dialog", panneau: () => _jsx(ListeChoix, { options: options, comptes: comptes, choisies: choisies, ecrire: ecrire, nom: nom, t: t }) }));
600
+ }
601
+ /** Plusieurs valeurs à la fois (« Acte » OU « Contrat ») : des cases, avec
602
+ * le nombre de lignes de chacune, une recherche au-delà de huit options,
603
+ * et « Tout » / « Aucun » sur ce que la recherche montre. */
604
+ function ListeChoix({ options, comptes, choisies, ecrire, nom, t, }) {
605
+ const [q, setQ] = useState('');
606
+ const vues = q.trim() ? options.filter((o) => normaliser(o).includes(normaliser(q.trim()))) : options;
607
+ return (_jsxs(Popover, { className: "zv-grille__plage", role: "dialog", "aria-label": t.filtrerColonne(nom), children: [options.length > 8 && (_jsx(Search, { value: q, onChange: (e) => setQ(e.target.value), placeholder: t.rechercherValeur, "aria-label": t.rechercherValeur })), _jsxs("div", { className: "zv-grille__menu-ligne", children: [_jsx(Button, { variante: "fantome", compact: true, onClick: () => ecrire([...new Set([...choisies, ...vues])]), children: t.tous }), _jsx(Button, { variante: "fantome", compact: true, onClick: () => ecrire(choisies.filter((c) => !vues.includes(c))), children: t.aucun })] }), _jsxs("div", { className: "zv-grille__liste", role: "group", "aria-label": t.filtrerColonne(nom), children: [vues.map((o) => {
608
+ const pris = choisies.includes(o);
609
+ return (_jsxs("label", { className: "zv-menu__item zv-grille__menu-case", children: [_jsx(Checkbox, { checked: pris, onChange: () => ecrire(pris ? choisies.filter((c) => c !== o) : [...choisies, o]) }), _jsx("span", { className: "zv-colhead__label", children: o }), _jsx(Meta, { taille: "sm", discret: true, className: "zv-grille__compte", children: comptes.get(o) ?? 0 })] }, o));
610
+ }), vues.length === 0 && _jsx(Meta, { taille: "sm", children: t.aucuneValeur })] }), choisies.length > 0 && (_jsx(Button, { variante: "fantome", compact: true, icone: "fermer", onClick: () => ecrire([]), children: t.effacer }))] }));
595
611
  }
596
612
  function FiltrePlage({ colonne, type, nom, t, }) {
597
613
  const idChamp = useId();
@@ -626,7 +642,7 @@ function FiltrePlage({ colonne, type, nom, t, }) {
626
642
  }) })), _jsxs("div", { className: "zv-grille__bornes", children: [_jsx(Field, { label: type === 'date' ? t.du : t.min, htmlFor: idChamp + 'a', children: champ(brouillon[0], (v) => saisir(v, brouillon[1]), 'a', { max: brouillon[1] }) }), _jsx(Field, { label: type === 'date' ? t.au : t.max, htmlFor: idChamp + 'b', children: champ(brouillon[1], (v) => saisir(brouillon[0], v), 'b', { min: brouillon[0] }) })] }), !vide && (_jsx(Button, { variante: "fantome", compact: true, icone: "fermer", onClick: () => saisir('', ''), children: t.effacer }))] })) }));
627
643
  }
628
644
  /* ─── La grille ──────────────────────────────────────────────────────── */
629
- export function DataGrid({ tableId, data, columns, onRowClick, ligneActive, getRowId, selection = false, onSelectionChange, parPage = 25, recherche = true, filtres = true, exportCsv = true, vueInitiale, vide, outils, textes, className, }) {
645
+ export function DataGrid({ tableId, data, columns, onRowClick, ligneActive, getRowId, selection = false, onSelectionChange, triServeur, onRowContextMenu, onRowDoubleClick, onRowDragStart, ligneSaisie, groupe, rang, pied, dense, remplir, apresLignes, envelopperTableau, barre = true, champRecherche, visibilite, onVisibilite, onLignesAffichees, parPage = 25, recherche = true, filtres = true, exportCsv = true, vueInitiale, vide, outils, textes, className, }) {
630
646
  const t = { ...TEXTES_GRILLE, ...textes };
631
647
  const ids = useMemo(() => columns.map((c) => idDeColonne(c)).filter((id) => id != null), [columns]);
632
648
  const cleIds = ids.join('\u0000');
@@ -648,37 +664,76 @@ export function DataGrid({ tableId, data, columns, onRowClick, ligneActive, getR
648
664
  const reconcilier = useCallback((lu, d) => reconcilierVue(lu, d, ids),
649
665
  // eslint-disable-next-line react-hooks/exhaustive-deps
650
666
  [cleIds]);
651
- const [vue, setVue, reinitialiserVue] = usePersistedState(`table:${tableId}:v1`, defaut, reconcilier);
667
+ const [vue, setVue, reinitialiserVue] = usePersistedState(tableId ? `table:${tableId}:v1` : null, defaut, reconcilier);
652
668
  const [columnFilters, setColumnFilters] = useState([]);
653
669
  const [globalFilter, setGlobalFilter] = useState('');
654
- const [rowSelection, setRowSelection] = useState({});
670
+ const [selectionInterne, setSelectionInterne] = useState({});
655
671
  const [pageIndex, setPageIndex] = useState(0);
656
672
  const champ = (k) => (u) => setVue((v) => ({ ...v, [k]: appliquer(u, v[k]) }));
673
+ /* Sélection. Tenue par la grille (`true`) ou par l'app (un objet) : dans le
674
+ * second cas, l'état de TanStack se DÉDUIT des clés de l'app à chaque rendu,
675
+ * et chaque changement lui est rendu en clés. */
676
+ const avecCases = selection !== false;
677
+ const externe = typeof selection === 'object' ? selection : null;
678
+ const idLigne = (l, i) => getRowId?.(l, i) ?? String(i);
679
+ const cleDe = (l, i) => externe?.cle?.(l) ?? idLigne(l, i);
680
+ const rowSelection = externe
681
+ ? Object.fromEntries(
682
+ // Une ligne sans case ne se peint pas cochée, même si sa clé est
683
+ // dans l'ensemble de l'app : on la verrait choisie sans pouvoir
684
+ // la décocher.
685
+ data.flatMap((l, i) => externe.choisies.has(cleDe(l, i)) && externe.selectionnable?.(l) !== false
686
+ ? [[idLigne(l, i), true]]
687
+ : []))
688
+ : selectionInterne;
689
+ const surSelectionLignes = (u) => {
690
+ if (!externe)
691
+ return setSelectionInterne(u);
692
+ const n = appliquer(u, rowSelection);
693
+ const suite = new Set(externe.choisies);
694
+ // Seules les clés des lignes PRÉSENTES changent : une ligne cochée puis
695
+ // filtrée hors de l'écran, ou sur une autre page, reste cochée.
696
+ data.forEach((l, i) => {
697
+ if (externe.selectionnable?.(l) === false)
698
+ return;
699
+ const cle = cleDe(l, i);
700
+ if (n[idLigne(l, i)])
701
+ suite.add(cle);
702
+ else
703
+ suite.delete(cle);
704
+ });
705
+ externe.onChoisir(suite);
706
+ };
657
707
  // La colonne de cases est épinglée à gauche, toujours, et n'entre pas dans
658
708
  // la vue enregistrée : elle dépend de la prop, pas de l'utilisateur.
659
709
  const pinning = {
660
- left: selection ? [COLONNE_SELECTION, ...(vue.columnPinning.left ?? [])] : vue.columnPinning.left,
710
+ left: avecCases ? [COLONNE_SELECTION, ...(vue.columnPinning.left ?? [])] : vue.columnPinning.left,
661
711
  right: vue.columnPinning.right,
662
712
  };
663
713
  const pagination = {
664
714
  pageIndex,
665
715
  pageSize: parPage === false ? Math.max(data.length, 1) : vue.pageSize,
666
716
  };
717
+ // L'option de sélection est lue AU RENDU par les cases : les colonnes n'ont
718
+ // pas à se recalculer à chaque case cochée.
719
+ const optionsCases = useRef(externe);
720
+ optionsCases.current = externe;
667
721
  const colonnes = useMemo(() => {
668
722
  const avecFiltre = columns.map((c) => (c.filterFn ? c : { ...c, filterFn: filtreGrille }));
669
- return selection ? [colonneSelection(t), ...avecFiltre] : avecFiltre;
723
+ return avecCases ? [colonneSelection(t, optionsCases), ...avecFiltre] : avecFiltre;
670
724
  // eslint-disable-next-line react-hooks/exhaustive-deps
671
- }, [columns, selection, t.toutSelectionner, t.selectionnerLigne]);
725
+ }, [columns, avecCases, t.toutSelectionner, t.selectionnerLigne]);
726
+ const visibiliteCourante = visibilite ?? vue.columnVisibility;
672
727
  const table = useReactTable({
673
728
  data,
674
729
  columns: colonnes,
675
730
  getRowId,
676
731
  state: {
677
732
  columnOrder: vue.columnOrder,
678
- columnVisibility: vue.columnVisibility,
733
+ columnVisibility: visibiliteCourante,
679
734
  columnSizing: vue.columnSizing,
680
735
  columnPinning: pinning,
681
- sorting: vue.sorting,
736
+ sorting: triServeur ? triServeur.tri : vue.sorting,
682
737
  columnFilters,
683
738
  globalFilter,
684
739
  rowSelection,
@@ -686,12 +741,17 @@ export function DataGrid({ tableId, data, columns, onRowClick, ligneActive, getR
686
741
  },
687
742
  defaultColumn: { size: 150, minSize: 60 },
688
743
  onColumnOrderChange: champ('columnOrder'),
689
- onColumnVisibilityChange: champ('columnVisibility'),
744
+ onColumnVisibilityChange: onVisibilite
745
+ ? (u) => onVisibilite((v) => appliquer(u, v))
746
+ : champ('columnVisibility'),
690
747
  onColumnSizingChange: champ('columnSizing'),
691
- onSortingChange: champ('sorting'),
748
+ onSortingChange: triServeur
749
+ ? (u) => triServeur.onTri(appliquer(u, triServeur.tri))
750
+ : champ('sorting'),
751
+ manualSorting: Boolean(triServeur),
692
752
  onColumnPinningChange: (u) => setVue((v) => {
693
753
  const actuel = {
694
- left: selection ? [COLONNE_SELECTION, ...(v.columnPinning.left ?? [])] : v.columnPinning.left,
754
+ left: avecCases ? [COLONNE_SELECTION, ...(v.columnPinning.left ?? [])] : v.columnPinning.left,
695
755
  right: v.columnPinning.right,
696
756
  };
697
757
  const n = appliquer(u, actuel);
@@ -705,7 +765,7 @@ export function DataGrid({ tableId, data, columns, onRowClick, ligneActive, getR
705
765
  }),
706
766
  onColumnFiltersChange: setColumnFilters,
707
767
  onGlobalFilterChange: setGlobalFilter,
708
- onRowSelectionChange: setRowSelection,
768
+ onRowSelectionChange: surSelectionLignes,
709
769
  onPaginationChange: (u) => {
710
770
  const n = appliquer(u, pagination);
711
771
  setPageIndex(n.pageIndex);
@@ -713,14 +773,17 @@ export function DataGrid({ tableId, data, columns, onRowClick, ligneActive, getR
713
773
  setVue((v) => ({ ...v, pageSize: n.pageSize }));
714
774
  }
715
775
  },
716
- enableRowSelection: selection,
776
+ enableRowSelection: externe?.selectionnable
777
+ ? (r) => externe.selectionnable(r.original)
778
+ : avecCases,
717
779
  enableMultiSort: true,
718
780
  enableColumnResizing: true,
719
781
  columnResizeMode: 'onChange',
720
782
  enableColumnPinning: true,
721
- globalFilterFn: 'includesString',
783
+ // Sans casse ni accents, comme les filtres de colonne.
784
+ globalFilterFn: (ligne, colonne, q) => normaliser(ligne.getValue(colonne)).includes(normaliser(String(q).trim())),
722
785
  getCoreRowModel: getCoreRowModel(),
723
- getSortedRowModel: getSortedRowModel(),
786
+ getSortedRowModel: triServeur ? undefined : getSortedRowModel(),
724
787
  getFilteredRowModel: getFilteredRowModel(),
725
788
  getPaginationRowModel: getPaginationRowModel(),
726
789
  getFacetedRowModel: getFacetedRowModel(),
@@ -731,9 +794,11 @@ export function DataGrid({ tableId, data, columns, onRowClick, ligneActive, getR
731
794
  const surSelection = useRef(onSelectionChange);
732
795
  surSelection.current = onSelectionChange;
733
796
  useEffect(() => {
797
+ if (externe)
798
+ return;
734
799
  surSelection.current?.(table.getSelectedRowModel().rows.map((r) => r.original));
735
800
  // eslint-disable-next-line react-hooks/exhaustive-deps
736
- }, [rowSelection]);
801
+ }, [selectionInterne]);
737
802
  // Une page au-delà de la dernière (filtre resserré, données rechargées)
738
803
  // afficherait la ligne d'absence alors que des lignes existent.
739
804
  const nbPages = table.getPageCount();
@@ -741,17 +806,22 @@ export function DataGrid({ tableId, data, columns, onRowClick, ligneActive, getR
741
806
  if (pageIndex > 0 && pageIndex >= nbPages)
742
807
  setPageIndex(Math.max(0, nbPages - 1));
743
808
  }, [pageIndex, nbPages]);
744
- const reinitialiser = () => {
745
- reinitialiserVue();
809
+ const effacerFiltres = () => {
746
810
  setColumnFilters([]);
747
811
  setGlobalFilter('');
812
+ };
813
+ const reinitialiser = () => {
814
+ reinitialiserVue();
815
+ if (onVisibilite)
816
+ onVisibilite(() => defaut.columnVisibility);
817
+ effacerFiltres();
748
818
  setPageIndex(0);
749
819
  };
750
820
  /* Glisser-déposer des en-têtes. */
751
821
  const idDnd = useId();
752
822
  const capteurs = useSensors(
753
- // 4 px avant de saisir : sans ce seuil, un simple clic sur la poignée
754
- // démarre un déplacement.
823
+ // 4 px avant de saisir : sans ce seuil, un simple clic sur l'en-tête
824
+ // démarre un déplacement au lieu de trier.
755
825
  useSensor(PointerSensor, { activationConstraint: { distance: 4 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
756
826
  const deplacables = table.getCenterVisibleLeafColumns().map((c) => c.id);
757
827
  const nomDe = (id) => {
@@ -788,22 +858,62 @@ export function DataGrid({ tableId, data, columns, onRowClick, ligneActive, getR
788
858
  // eslint-disable-next-line react-hooks/exhaustive-deps
789
859
  }, [lignesBase, colonnes]);
790
860
  const filtrables = table.getVisibleLeafColumns().some((c) => c.getCanFilter() && types[c.id]);
791
- const rangeeFiltres = filtres && filtrables ? (_jsx("div", { className: "zv-table__head zv-table__head--filtres", children: colonnesAffichees(table).map((c) => {
861
+ /** Une rangée qui suit les colonnes affichées (ordre, épinglage) : les
862
+ * filtres, la ligne de saisie, le pied. */
863
+ const rangee = (classe, contenu) => (_jsx("div", { className: classe, children: colonnesAffichees(table).map((c) => {
792
864
  const pin = epinglage(c, table);
865
+ const registre = c.columnDef.meta?.registre;
866
+ return (_jsx("div", { className: cx('zv-cell', registre === 'num' && 'zv-cell--num', registre === 'nom' && 'zv-cell--name', pin.classes), style: pin.style, children: contenu(c) }, c.id));
867
+ }) }));
868
+ const rangeeFiltres = filtres && filtrables
869
+ ? rangee('zv-table__head zv-table__head--filtres', (c) => {
793
870
  const type = c.getCanFilter() ? types[c.id] : false;
794
- return (_jsx("div", { className: cx('zv-cell', pin.classes), style: pin.style, children: type && _jsx(FiltreColonne, { colonne: c, type: type, t: t }) }, c.id));
795
- }) })) : null;
796
- const total = table.getPrePaginationRowModel().rows.length;
871
+ return type ? _jsx(FiltreColonne, { colonne: c, type: type, t: t }) : null;
872
+ })
873
+ : null;
874
+ /* Lignes : le rang (des lignes gardées en tête), puis les intitulés de
875
+ * groupe et le pied, calculés sur ce qui est AFFICHÉ. */
876
+ const lignesTriees = table.getRowModel().rows;
877
+ const lignesRendues = useMemo(() => {
878
+ if (!rang)
879
+ return lignesTriees;
880
+ return lignesTriees
881
+ .map((r, i) => ({ r, i, k: rang(r.original) }))
882
+ .sort((a, b) => a.k - b.k || a.i - b.i)
883
+ .map((x) => x.r);
884
+ // eslint-disable-next-line react-hooks/exhaustive-deps
885
+ }, [lignesTriees, rang]);
886
+ const affichees = table.getPrePaginationRowModel().rows.map((r) => r.original);
887
+ const filtre = columnFilters.length > 0 || globalFilter.trim() !== '';
888
+ const surAffichees = useRef(onLignesAffichees);
889
+ surAffichees.current = onLignesAffichees;
890
+ const signatureAffichees = `${filtre}|${lignesRendues.map((r) => r.id).join('\u0001')}`;
891
+ useEffect(() => {
892
+ surAffichees.current?.(lignesRendues.map((r) => r.original), filtre);
893
+ // eslint-disable-next-line react-hooks/exhaustive-deps
894
+ }, [signatureAffichees]);
895
+ const intercalaire = groupe
896
+ ? (ligne, precedente) => {
897
+ const cle = groupe.de(ligne.original);
898
+ if (precedente && groupe.de(precedente.original) === cle)
899
+ return null;
900
+ const membres = lignesRendues.filter((r) => groupe.de(r.original) === cle).map((r) => r.original);
901
+ return (_jsx("div", { className: "zv-row zv-row--groupe", children: groupe.rendu ? groupe.rendu(cle, membres) : _jsx(Mono, { ton: "label", children: cle }) }));
902
+ }
903
+ : undefined;
904
+ const casesPied = pied && lignesRendues.length > 0 ? pied(affichees) : null;
905
+ const total = affichees.length;
797
906
  const { pageIndex: page, pageSize } = table.getState().pagination;
798
907
  const debut = total === 0 ? 0 : page * pageSize + 1;
799
908
  const fin = Math.min(total, (page + 1) * pageSize);
800
- const nbChoisies = Object.keys(rowSelection).length;
801
- const filtre = columnFilters.length > 0 || globalFilter !== '';
802
- return (_jsxs("div", { className: cx('zv-grille', className), children: [_jsxs("div", { className: "zv-grille__outils", children: [recherche ? (_jsx(Search, { value: globalFilter, onChange: (e) => setGlobalFilter(e.target.value), placeholder: t.rechercher, "aria-label": t.rechercher })) : (_jsx("span", {})), _jsxs("div", { className: "zv-grille__actions", children: [selection && nbChoisies > 0 && (_jsx(Meta, { children: t.choisies(nbChoisies) })), outils, _jsx(MenuColonnes, { table: table, t: t }), exportCsv !== false && (_jsx(Button, { variante: "fantome", compact: true, icone: "telecharger", onClick: () => telecharger(versCsv(table), typeof exportCsv === 'string' ? exportCsv : tableId), children: t.exporter })), _jsx(Button, { variante: "fantome", compact: true, icone: "rembobiner", onClick: reinitialiser, children: t.reinitialiser })] })] }), _jsx(DndContext, { id: idDnd, sensors: capteurs, collisionDetection: closestCenter, modifiers: [axeHorizontal], onDragEnd: finDeplacement, accessibility: {
803
- announcements: annonces,
804
- screenReaderInstructions: {
805
- draggable: t.dndInstructions,
806
- },
807
- }, children: _jsx(SortableContext, { items: deplacables, strategy: horizontalListSortingStrategy, children: _jsx(DataTable, { table: table, className: "zv-table--grille", celluleEntete: (p) => _jsx(EnteteColonne, { ...p, t: t }), sousEntete: rangeeFiltres, onRowClick: onRowClick, ligneActive: ligneActive, vide: vide ?? (_jsx(Empty, { children: data.length === 0 ? t.aucuneDonnee : filtre ? t.aucunResultat : t.aucuneLigne })) }) }) }), parPage !== false && (_jsxs(Pager, { pied: true, "aria-label": t.pagination, children: [_jsx(Meta, { children: t.compte(debut, fin, total) }), _jsxs("div", { className: "zv-grille__pied", children: [_jsx(SelectMenu, { className: "zv-grille__taille", label: t.lignesParPage, valeur: String(pageSize), options: TAILLES_PAGE.map((taille) => ({ valeur: String(taille), label: t.parPage(taille) })), onChange: (v) => table.setPageSize(Number(v)) }), _jsx(PagerArrow, { "aria-label": t.pagePrecedente, desactive: !table.getCanPreviousPage(), onClick: () => table.getCanPreviousPage() && table.previousPage(), children: "\u2039" }), _jsx(Meta, { children: t.page(nbPages === 0 ? 0 : page + 1, nbPages) }), _jsx(PagerArrow, { "aria-label": t.pageSuivante, desactive: !table.getCanNextPage(), onClick: () => table.getCanNextPage() && table.nextPage(), children: "\u203A" })] })] }))] }));
909
+ const nbChoisies = externe ? externe.choisies.size : Object.keys(selectionInterne).length;
910
+ const tableau = (_jsx(DndContext, { id: idDnd, sensors: capteurs, collisionDetection: closestCenter, modifiers: [axeHorizontal], onDragEnd: finDeplacement, accessibility: {
911
+ announcements: annonces,
912
+ screenReaderInstructions: {
913
+ draggable: t.dndInstructions,
914
+ },
915
+ }, children: _jsx(SortableContext, { items: deplacables, strategy: horizontalListSortingStrategy, children: _jsx(DataTable, { table: table, className: "zv-table--grille", celluleEntete: (p) => _jsx(EnteteColonne, { ...p, t: t }), sousEntete: rangeeFiltres, onRowClick: onRowClick, ligneActive: ligneActive, onRowContextMenu: onRowContextMenu, onRowDoubleClick: onRowDoubleClick, onRowDragStart: onRowDragStart, lignes: rang ? lignesRendues : undefined, intercalaire: intercalaire, avantLignes: ligneSaisie ? rangee('zv-row zv-row--saisie', (c) => ligneSaisie[c.id] ?? null) : undefined, apresLignes: _jsxs(_Fragment, { children: [casesPied && rangee('zv-row zv-row--pied', (c) => casesPied[c.id] ?? null), apresLignes] }), dense: dense, remplir: remplir, vide: vide && !filtre ? (vide) : (_jsxs(Empty, { children: [data.length === 0 ? t.aucuneDonnee : filtre ? t.aucunResultat : t.aucuneLigne, ' ', filtre && (_jsx(Button, { variante: "fantome", compact: true, onClick: effacerFiltres, children: t.effacerFiltres }))] })) }) }) }));
916
+ return (_jsxs("div", { className: cx('zv-grille', remplir && 'zv-grille--remplir', className), children: [barre && (_jsxs("div", { className: "zv-grille__outils", children: [champRecherche ??
917
+ (recherche !== false ? (_jsx(Search, { value: globalFilter, onChange: (e) => setGlobalFilter(e.target.value), placeholder: typeof recherche === 'string' ? recherche : t.rechercher, "aria-label": t.rechercher })) : (_jsx("span", {}))), _jsxs("div", { className: "zv-grille__actions", children: [avecCases && nbChoisies > 0 && _jsx(Meta, { children: t.choisies(nbChoisies) }), outils, !onVisibilite && _jsx(MenuColonnes, { table: table, t: t }), exportCsv !== false && (_jsx(Button, { variante: "fantome", compact: true, icone: "telecharger", onClick: () => telecharger(versCsv(table), typeof exportCsv === 'string' ? exportCsv : (tableId ?? 'export')), children: t.exporter })), _jsx(Button, { variante: "fantome", compact: true, icone: "rembobiner", onClick: reinitialiser, children: t.reinitialiser })] })] })), envelopperTableau ? envelopperTableau(tableau) : tableau, parPage !== false && (_jsxs(Pager, { pied: true, "aria-label": t.pagination, children: [_jsx(Meta, { children: t.compte(debut, fin, total) }), _jsxs("div", { className: "zv-grille__pied", children: [_jsx(SelectMenu, { className: "zv-grille__taille", label: t.lignesParPage, valeur: String(pageSize), options: TAILLES_PAGE.map((taille) => ({ valeur: String(taille), label: t.parPage(taille) })), onChange: (v) => table.setPageSize(Number(v)) }), _jsx(PagerArrow, { "aria-label": t.pagePrecedente, desactive: !table.getCanPreviousPage(), onClick: () => table.getCanPreviousPage() && table.previousPage(), children: "\u2039" }), _jsx(Meta, { children: t.page(nbPages === 0 ? 0 : page + 1, nbPages) }), _jsx(PagerArrow, { "aria-label": t.pageSuivante, desactive: !table.getCanNextPage(), onClick: () => table.getCanNextPage() && table.nextPage(), children: "\u203A" })] })] }))] }));
808
918
  }
809
919
  //# sourceMappingURL=grille.js.map