Fait saisir un par un ce qui se saisissait sur une ligne à séparateurs
Les thèmes et les auteurs souffraient du défaut corrigé hier sur les unes : un champ relié à chaque frappe sur une propriété qui découpait puis recomposait la liste, donc un séparateur intapable. Ils passent au même geste — écrire, Entrée, puis modifier ou retirer chaque ligne — et le composant est partagé plutôt que recopié une troisième fois. Le bloc des rôles se fond dans la liste des auteurs : il listait les mêmes noms une seconde fois. Renommer une ligne conserve son rôle, parce qu'on corrige un nom au lieu de reconstruire la liste. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,77 +32,15 @@
|
||||
</div>
|
||||
|
||||
@*
|
||||
⚠️ Un titre à la fois, et surtout PAS une ligne de titres séparés par des points-virgules.
|
||||
Le champ unique se reliait à chaque frappe sur une propriété qui découpait puis recomposait
|
||||
la liste : la saisie était donc réécrite sous les doigts — un point-virgule tapé disparaissait
|
||||
(il ouvrait une valeur vide, aussitôt élaguée), et l'espace qui le suit était rogné par le
|
||||
même élagage. Autrement dit, le séparateur du champ était le seul caractère qu'on ne pouvait
|
||||
pas y taper.
|
||||
|
||||
Ici la liste est la donnée : on écrit un titre, Entrée l'y range, et chaque titre se modifie
|
||||
ou se retire ensuite. Une virgule (« Ukraine, deux ans après ») n'a plus rien à couper,
|
||||
puisque plus rien ne découpe.
|
||||
⚠️ Un titre à la fois, et surtout PAS une ligne de titres séparés par des points-virgules :
|
||||
voir SaisieListe, qui porte le pourquoi. Une virgule dans un titre (« Ukraine, deux ans
|
||||
après ») n'a plus rien à couper, puisque plus rien ne découpe.
|
||||
*@
|
||||
<div class="champ">
|
||||
<label class="champ-libelle" for="@_idChamp">
|
||||
À la une <span class="champ-aide">(un titre à la fois : tapez-le, puis Entrée)</span>
|
||||
</label>
|
||||
|
||||
<div class="ligne-creation">
|
||||
<input id="@_idChamp" class="champ-saisie" type="text" @bind="_titreSaisi"
|
||||
@bind:event="oninput" @onkeydown="SurToucheArticle"
|
||||
placeholder="Ukraine, deux ans après" autocomplete="off" />
|
||||
|
||||
<button type="button" class="bouton bouton-compact"
|
||||
disabled="@(string.IsNullOrWhiteSpace(_titreSaisi))" @onclick="ValiderArticle">
|
||||
@(_indexModifie is null ? "Ajouter" : "Enregistrer")
|
||||
</button>
|
||||
|
||||
@if (_indexModifie is not null)
|
||||
{
|
||||
<button type="button" class="bouton bouton-compact bouton-discret"
|
||||
@onclick="AnnulerModification">
|
||||
Annuler
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (_messageArticle is not null)
|
||||
{
|
||||
<p class="message-discret" role="status">@_messageArticle</p>
|
||||
}
|
||||
|
||||
@if (Saisie.Articles.Count > 0)
|
||||
{
|
||||
<ul class="liste-unes">
|
||||
@for (var rang = 0; rang < Saisie.Articles.Count; rang++)
|
||||
{
|
||||
@* La copie locale est indispensable : sans elle, les fermetures des deux
|
||||
boutons captureraient la variable de boucle, donc son état final. *@
|
||||
var index = rang;
|
||||
var titre = Saisie.Articles[rang];
|
||||
|
||||
<li class="ligne-une" @key="index">
|
||||
<span class="ligne-une-titre">@titre</span>
|
||||
|
||||
<div class="actions-tome">
|
||||
<button type="button" class="bouton bouton-compact"
|
||||
aria-label="@($"Modifier « {titre} »")"
|
||||
@onclick="() => ModifierArticle(index)">
|
||||
Modifier
|
||||
</button>
|
||||
|
||||
<button type="button" class="bouton bouton-compact bouton-danger"
|
||||
aria-label="@($"Supprimer « {titre} »")"
|
||||
@onclick="() => SupprimerArticle(index)">
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
<SaisieListe Valeurs="Saisie.Articles" Libelle="À la une"
|
||||
Aide=" (un titre à la fois : tapez-le, puis Entrée)"
|
||||
Placeholder="Ukraine, deux ans après"
|
||||
MessageDoublon="Ce titre est déjà à la une de ce numéro."
|
||||
OnChangement="Notifier" />
|
||||
|
||||
@*
|
||||
⚠️ Une URL, pas une photo. Aucune source ne donne la couverture d'un NUMÉRO : l'ISSN désigne
|
||||
@@ -165,108 +103,6 @@
|
||||
/// </remarks>
|
||||
private void Notifier() => _ = OnChangement.InvokeAsync();
|
||||
|
||||
/// <summary>Identifiant du champ « à la une », pour que son libellé le désigne vraiment.</summary>
|
||||
private readonly string _idChamp = $"une-{Guid.NewGuid():N}";
|
||||
|
||||
/// <summary>Titre en cours de frappe. <b>Il n'est relié à rien d'autre</b> : voir plus haut.</summary>
|
||||
private string? _titreSaisi;
|
||||
|
||||
/// <summary>Rang du titre qu'on modifie — <c>null</c> = on en ajoute un nouveau.</summary>
|
||||
private int? _indexModifie;
|
||||
|
||||
private string? _messageArticle;
|
||||
|
||||
/// <summary>Saisie servie au rendu précédent, pour repérer que le parent en a changé.</summary>
|
||||
private AjoutNumeroRevue? _saisiePrecedente;
|
||||
|
||||
/// <summary>
|
||||
/// Le parent remplace la saisie entière (numéro enregistré, autre numéro ouvert en
|
||||
/// modification) : ce qui restait dans le champ appartenait au numéro d'avant.
|
||||
/// </summary>
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (!ReferenceEquals(_saisiePrecedente, Saisie))
|
||||
{
|
||||
_saisiePrecedente = Saisie;
|
||||
RemettreAZero();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemettreAZero()
|
||||
{
|
||||
_titreSaisi = null;
|
||||
_indexModifie = null;
|
||||
_messageArticle = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Entrée range le titre, comme le bouton.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// ⚠️ Cette touche ne remonte <b>pas</b> à <c>OnEntree</c> : celui-ci enregistre le numéro
|
||||
/// entier, et l'on serait alors bien en peine de saisir deux titres à la une.
|
||||
/// </remarks>
|
||||
private void SurToucheArticle(KeyboardEventArgs e)
|
||||
{
|
||||
if (e.Key == "Enter")
|
||||
{
|
||||
ValiderArticle();
|
||||
}
|
||||
}
|
||||
|
||||
private void ValiderArticle()
|
||||
{
|
||||
var titre = _titreSaisi?.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(titre))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Même règle de doublon que partout ailleurs : au sens de la normalisation, et le
|
||||
// libellé déjà rangé est celui qui reste.
|
||||
var cle = NormalisationTexte.Normaliser(titre);
|
||||
var deja = Saisie.Articles.FindIndex(a => NormalisationTexte.Normaliser(a) == cle);
|
||||
|
||||
if (deja >= 0 && deja != _indexModifie)
|
||||
{
|
||||
_messageArticle = "Ce titre est déjà à la une de ce numéro.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (_indexModifie is { } index)
|
||||
{
|
||||
Saisie.Articles[index] = titre;
|
||||
}
|
||||
else
|
||||
{
|
||||
Saisie.Articles.Add(titre);
|
||||
}
|
||||
|
||||
RemettreAZero();
|
||||
Notifier();
|
||||
}
|
||||
|
||||
/// <summary>Reprend un titre dans le champ : on le corrige là où on l'a écrit.</summary>
|
||||
private void ModifierArticle(int index)
|
||||
{
|
||||
_messageArticle = null;
|
||||
_indexModifie = index;
|
||||
_titreSaisi = Saisie.Articles[index];
|
||||
}
|
||||
|
||||
private void AnnulerModification() => RemettreAZero();
|
||||
|
||||
private void SupprimerArticle(int index)
|
||||
{
|
||||
Saisie.Articles.RemoveAt(index);
|
||||
|
||||
// Le titre qu'on modifiait vient peut-être de disparaître, et les rangs suivants ont
|
||||
// glissé : reprendre la modification écrirait sur le mauvais titre.
|
||||
RemettreAZero();
|
||||
Notifier();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Date affichée par le champ, en <b>local</b>.
|
||||
/// </summary>
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
@*
|
||||
⚠️ L'ORDRE DES CHAMPS EST UNE DÉCISION (lot Q, 2026-08-21), pas une mise en page.
|
||||
|
||||
Type de document, puis thèmes, puis auteurs, puis rôles : c'est l'ordre dans lequel on
|
||||
regarde un livre qu'on tient en main, et surtout le type COMMANDE LA SUITE de la saisie —
|
||||
les rôles ne se posent que là. Relégué en bas, il obligeait à descendre tout le formulaire
|
||||
Type de document, puis thèmes, puis auteurs (chacun avec son rôle) : c'est l'ordre dans
|
||||
lequel on regarde un livre qu'on tient en main, et surtout le type COMMANDE LA SUITE de
|
||||
la saisie — les rôles ne se posent que là. Relégué en bas, il obligeait à descendre tout le formulaire
|
||||
pour dire « c'est une BD », c'est-à-dire après avoir saisi ce qui en dépend.
|
||||
|
||||
⚠️ Remonter le champ le rend visible ; le PRÉREMPLIR écrirait quelque chose de faux. Le
|
||||
@@ -46,61 +46,48 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<label class="champ">
|
||||
@* Point-virgule, comme le champ des auteurs juste en dessous : trois champs voisins du
|
||||
même formulaire ne doivent pas se saisir de trois façons. La virgule reste acceptée —
|
||||
un thème n'en contient jamais, contrairement à un titre d'article à la une. *@
|
||||
<span class="champ-libelle">
|
||||
Thèmes <span class="champ-aide">(séparés par des points-virgules)</span>
|
||||
</span>
|
||||
<input class="champ-saisie" type="text" @bind="Saisie.ThemesTexte" @bind:event="oninput"
|
||||
placeholder="Fantasy ; aventure ; space opera" autocomplete="off" />
|
||||
</label>
|
||||
@*
|
||||
⚠️ Thèmes et auteurs se saisissent UN PAR UN, et non plus sur une ligne à séparateurs.
|
||||
Le champ unique se reliait à chaque frappe sur une propriété qui découpait puis
|
||||
recomposait la liste : le point-virgule tapé y était élagué (entrée vide) et l'espace
|
||||
qui le suit rogné — le séparateur était le seul caractère qu'on ne pouvait pas taper.
|
||||
Voir CLAUDE.md.
|
||||
*@
|
||||
<SaisieListe Valeurs="Saisie.Themes" Libelle="Thèmes" Aide=" (un par un : tapez, puis Entrée)"
|
||||
Placeholder="dark fantasy" MessageDoublon="Ce thème est déjà sur ce livre."
|
||||
OnChangement="StateHasChanged" />
|
||||
|
||||
@*
|
||||
Un seul champ, plusieurs auteurs : le point-virgule les sépare. La virgule est déjà
|
||||
prise par la forme bibliographique « Zola, Émile », qu'on doit pouvoir taper telle quelle.
|
||||
Le serveur rapproche ensuite les variantes sûres d'un auteur déjà connu.
|
||||
*@
|
||||
<label class="champ">
|
||||
<span class="champ-libelle">
|
||||
Auteur<span class="champ-aide"> (plusieurs : séparez par « ; »)</span>
|
||||
</span>
|
||||
<input class="champ-saisie" type="text" @bind="Saisie.Auteur" @bind:event="oninput"
|
||||
placeholder="Prénom Nom ; Prénom Nom" autocomplete="off" />
|
||||
</label>
|
||||
Un auteur par ligne, et son RÔLE avec lui.
|
||||
|
||||
@*
|
||||
Les rôles n'apparaissent qu'à partir de DEUX auteurs, et c'est délibéré : un roman n'a
|
||||
qu'un signataire dont le rôle ne se pose pas, et une ligne de boutons sous chaque saisie
|
||||
n'apprendrait rien. La question « qui écrit, qui dessine ? » ne se pose qu'à plusieurs.
|
||||
Le défaut reste « non précisé » : on ne remplit pas une case à la place de l'utilisateur.
|
||||
*@
|
||||
@if (Saisie.Auteurs.Count > 1)
|
||||
{
|
||||
<fieldset class="champ">
|
||||
<legend class="champ-libelle">
|
||||
Rôles<span class="champ-aide"> (facultatif — utile pour une BD)</span>
|
||||
</legend>
|
||||
⚠️ Le rôle vit sur le LIEN, pas sur la personne : le même auteur scénarise un album et
|
||||
dessine le suivant. C'est aussi pourquoi renommer une ligne conserve son rôle
|
||||
(NomsAuteurs) — on corrige un nom, on ne désigne pas quelqu'un d'autre.
|
||||
|
||||
@foreach (var (auteur, index) in Saisie.Auteurs.Select((a, i) => (a, i)))
|
||||
⚠️ Les rôles n'apparaissent qu'à partir de DEUX auteurs, décision inchangée : un roman
|
||||
n'a qu'un signataire dont le rôle ne se pose pas. Le défaut reste « non précisé ».
|
||||
*@
|
||||
<SaisieListe Valeurs="_noms" Libelle="Auteurs" Aide=" (un par un : tapez, puis Entrée)"
|
||||
Placeholder="Prénom Nom, ou Nom, Prénom"
|
||||
MessageDoublon="Cet auteur est déjà sur ce livre."
|
||||
OnChangement="StateHasChanged">
|
||||
<Complement Context="index">
|
||||
@if (Saisie.Auteurs.Count > 1)
|
||||
{
|
||||
<div class="ligne-role" @key="index">
|
||||
<span class="nom-role">@auteur.Nom</span>
|
||||
<div class="segments">
|
||||
@foreach (var role in Roles)
|
||||
{
|
||||
<button type="button"
|
||||
class="segment @(auteur.Role == role ? "segment-actif" : null)"
|
||||
@onclick="() => PoserRole(index, role)">
|
||||
@Libelles.RoleAuteur(role)
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="segments segments-role" role="group"
|
||||
aria-label="@($"Rôle de {Saisie.Auteurs[index].Nom}")">
|
||||
@foreach (var role in Roles)
|
||||
{
|
||||
<button type="button"
|
||||
class="segment @(Saisie.Auteurs[index].Role == role ? "segment-actif" : null)"
|
||||
@onclick="() => PoserRole(index, role)">
|
||||
@Libelles.RoleAuteur(role)
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</fieldset>
|
||||
}
|
||||
</Complement>
|
||||
</SaisieListe>
|
||||
|
||||
@*
|
||||
Le format n'est plus affiché dans les listes que pour les ebooks : un livre y est donc
|
||||
@@ -212,6 +199,27 @@
|
||||
private void PoserRole(int index, RoleAuteur role) =>
|
||||
Saisie.Auteurs[index] = Saisie.Auteurs[index] with { Role = role };
|
||||
|
||||
/// <summary>
|
||||
/// Les noms des auteurs, vus comme une liste de chaînes par <c>SaisieListe</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// ⚠️ Sa <b>référence doit rester stable</b> tant que la saisie ne change pas : le composant
|
||||
/// se remet à zéro quand la liste qu'on lui prête change d'identité, et il perdrait donc à
|
||||
/// chaque rendu ce qui est en cours de frappe.
|
||||
/// </remarks>
|
||||
private NomsAuteurs _noms = default!;
|
||||
|
||||
private EnregistrementLivre? _saisiePrecedente;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (!ReferenceEquals(_saisiePrecedente, Saisie))
|
||||
{
|
||||
_saisiePrecedente = Saisie;
|
||||
_noms = new NomsAuteurs(Saisie.Auteurs);
|
||||
}
|
||||
}
|
||||
|
||||
[Parameter, EditorRequired] public EnregistrementLivre Saisie { get; set; } = new();
|
||||
|
||||
[Parameter] public string LibelleValidation { get; set; } = "Enregistrer";
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Collections;
|
||||
using MaBibli.Shared.Dtos;
|
||||
|
||||
namespace MaBibli.Client.Composants;
|
||||
|
||||
/// <summary>
|
||||
/// Vue « liste de noms » des auteurs d'une saisie, pour que <c>SaisieListe</c> les remplisse
|
||||
/// comme il remplit les thèmes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// ⚠️ <b>Un auteur n'est pas un nom : il porte aussi son rôle sur ce livre</b> (scénario,
|
||||
/// dessin…), qui vit sur le lien et non sur la personne. Renommer par l'indexeur <b>conserve
|
||||
/// donc le rôle</b> — c'est ce qui remplace la reprise des rôles que faisait
|
||||
/// <c>EnregistrementLivre.Decouper</c> quand le champ des noms se reconstruisait à chaque
|
||||
/// frappe : ici la ligne n'est pas reconstruite, elle est corrigée.
|
||||
/// <para>
|
||||
/// La liste sous-jacente est celle de la saisie, modifiée en place : cette vue n'en garde
|
||||
/// aucune copie qui pourrait diverger.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class NomsAuteurs(IList<AuteurSaisi> auteurs) : IList<string>
|
||||
{
|
||||
public int Count => auteurs.Count;
|
||||
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
public string this[int index]
|
||||
{
|
||||
get => auteurs[index].Nom;
|
||||
set => auteurs[index] = auteurs[index] with { Nom = value };
|
||||
}
|
||||
|
||||
public void Add(string nom) => auteurs.Add(new AuteurSaisi(nom));
|
||||
|
||||
public void Insert(int index, string nom) => auteurs.Insert(index, new AuteurSaisi(nom));
|
||||
|
||||
public void RemoveAt(int index) => auteurs.RemoveAt(index);
|
||||
|
||||
public void Clear() => auteurs.Clear();
|
||||
|
||||
public bool Contains(string nom) => IndexOf(nom) >= 0;
|
||||
|
||||
public int IndexOf(string nom)
|
||||
{
|
||||
for (var i = 0; i < auteurs.Count; i++)
|
||||
{
|
||||
if (auteurs[i].Nom == nom)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public bool Remove(string nom)
|
||||
{
|
||||
var index = IndexOf(nom);
|
||||
|
||||
if (index < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auteurs.RemoveAt(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void CopyTo(string[] tableau, int index)
|
||||
{
|
||||
foreach (var auteur in auteurs)
|
||||
{
|
||||
tableau[index++] = auteur.Nom;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<string> GetEnumerator() => auteurs.Select(a => a.Nom).GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
@*
|
||||
Un champ qui remplit une LISTE : on écrit une valeur, Entrée la range, et chaque ligne se
|
||||
modifie ou se retire ensuite.
|
||||
|
||||
⚠️ Il remplace partout le « champ unique à séparateur » (thèmes, articles à la une), qui
|
||||
était intapable : relié à chaque frappe sur une propriété qui découpait puis recomposait la
|
||||
ligne, il élaguait le point-virgule tapé (entrée vide) et rognait l'espace qui le suit. Le
|
||||
seul caractère qu'on ne pouvait pas y taper était son propre séparateur. Voir CLAUDE.md.
|
||||
|
||||
⚠️ Le composant écrit dans une liste PRÊTÉE par son parent : il doit donc le prévenir
|
||||
(OnChangement), sinon tout ce que le parent calcule à partir d'elle reste figé.
|
||||
*@
|
||||
<div class="champ">
|
||||
<label class="champ-libelle" for="@_idChamp">
|
||||
@Libelle
|
||||
@if (!string.IsNullOrWhiteSpace(Aide))
|
||||
{
|
||||
<span class="champ-aide">@Aide</span>
|
||||
}
|
||||
</label>
|
||||
|
||||
<div class="ligne-creation">
|
||||
<input id="@_idChamp" class="champ-saisie" type="text" @bind="_saisie"
|
||||
@bind:event="oninput" @onkeydown="SurTouche"
|
||||
placeholder="@Placeholder" autocomplete="off" />
|
||||
|
||||
<button type="button" class="bouton bouton-compact"
|
||||
disabled="@(string.IsNullOrWhiteSpace(_saisie))" @onclick="Valider">
|
||||
@(_indexModifie is null ? "Ajouter" : "Enregistrer")
|
||||
</button>
|
||||
|
||||
@if (_indexModifie is not null)
|
||||
{
|
||||
<button type="button" class="bouton bouton-compact bouton-discret"
|
||||
@onclick="Annuler">
|
||||
Annuler
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (_message is not null)
|
||||
{
|
||||
<p class="message-discret" role="status">@_message</p>
|
||||
}
|
||||
|
||||
@if (Valeurs.Count > 0)
|
||||
{
|
||||
<ul class="liste-saisie">
|
||||
@for (var rang = 0; rang < Valeurs.Count; rang++)
|
||||
{
|
||||
@* La copie locale est indispensable : sans elle, les fermetures des boutons
|
||||
captureraient la variable de boucle, donc son état final. *@
|
||||
var index = rang;
|
||||
var valeur = Valeurs[rang];
|
||||
|
||||
<li class="ligne-saisie" @key="index">
|
||||
<span class="ligne-saisie-valeur">@valeur</span>
|
||||
|
||||
@* Ce que le parent veut poser sur la ligne — les rôles d'un auteur, par
|
||||
exemple. Rien par défaut. *@
|
||||
@Complement?.Invoke(index)
|
||||
|
||||
<div class="actions-tome">
|
||||
<button type="button" class="bouton bouton-compact"
|
||||
aria-label="@($"Modifier « {valeur} »")"
|
||||
@onclick="() => Modifier(index)">
|
||||
Modifier
|
||||
</button>
|
||||
|
||||
<button type="button" class="bouton bouton-compact bouton-danger"
|
||||
aria-label="@($"Supprimer « {valeur} »")"
|
||||
@onclick="() => Supprimer(index)">
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>La liste remplie, modifiée <b>en place</b> : c'est celle du parent.</summary>
|
||||
[Parameter, EditorRequired] public IList<string> Valeurs { get; set; } = default!;
|
||||
|
||||
[Parameter, EditorRequired] public string Libelle { get; set; } = default!;
|
||||
|
||||
[Parameter] public string? Aide { get; set; }
|
||||
|
||||
[Parameter] public string? Placeholder { get; set; }
|
||||
|
||||
/// <summary>Message affiché quand la valeur tapée est déjà dans la liste.</summary>
|
||||
[Parameter] public string MessageDoublon { get; set; } = "C'est déjà dans la liste.";
|
||||
|
||||
/// <summary>
|
||||
/// Ce que le parent ajoute à chaque ligne — les rôles d'un auteur, par exemple.
|
||||
/// </summary>
|
||||
[Parameter] public RenderFragment<int>? Complement { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Prévient le parent que la liste a changé.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// ⚠️ Sans cela, un bouton du parent qui dépend de la liste reste figé : Blazor ne redessine
|
||||
/// que le composant qui a traité l'événement.
|
||||
/// </remarks>
|
||||
[Parameter] public EventCallback OnChangement { get; set; }
|
||||
|
||||
private readonly string _idChamp = $"liste-{Guid.NewGuid():N}";
|
||||
|
||||
/// <summary>Valeur en cours de frappe. <b>Elle n'est reliée à rien d'autre</b>.</summary>
|
||||
private string? _saisie;
|
||||
|
||||
/// <summary>Rang de la valeur qu'on modifie — <c>null</c> = on en ajoute une nouvelle.</summary>
|
||||
private int? _indexModifie;
|
||||
|
||||
private string? _message;
|
||||
|
||||
/// <summary>Liste servie au rendu précédent, pour repérer que le parent en a changé.</summary>
|
||||
private IList<string>? _precedente;
|
||||
|
||||
/// <summary>
|
||||
/// Le parent a changé de liste (autre fiche, formulaire vidé après enregistrement) : ce qui
|
||||
/// restait dans le champ appartenait à la précédente.
|
||||
/// </summary>
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (!ReferenceEquals(_precedente, Valeurs))
|
||||
{
|
||||
_precedente = Valeurs;
|
||||
RemettreAZero();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemettreAZero()
|
||||
{
|
||||
_saisie = null;
|
||||
_indexModifie = null;
|
||||
_message = null;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// ⚠️ Cette touche ne remonte pas au parent : celui-ci enregistre la fiche entière, et l'on
|
||||
/// serait bien en peine de saisir deux valeurs.
|
||||
/// </remarks>
|
||||
private void SurTouche(KeyboardEventArgs e)
|
||||
{
|
||||
if (e.Key == "Enter")
|
||||
{
|
||||
Valider();
|
||||
}
|
||||
}
|
||||
|
||||
private void Valider()
|
||||
{
|
||||
var valeur = _saisie?.Trim();
|
||||
|
||||
if (string.IsNullOrEmpty(valeur))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Doublon au sens de la normalisation, comme partout ailleurs : le libellé déjà rangé
|
||||
// est celui qui reste.
|
||||
var cle = NormalisationTexte.Normaliser(valeur);
|
||||
var deja = Valeurs.ToList().FindIndex(v => NormalisationTexte.Normaliser(v) == cle);
|
||||
|
||||
if (deja >= 0 && deja != _indexModifie)
|
||||
{
|
||||
_message = MessageDoublon;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_indexModifie is { } index)
|
||||
{
|
||||
Valeurs[index] = valeur;
|
||||
}
|
||||
else
|
||||
{
|
||||
Valeurs.Add(valeur);
|
||||
}
|
||||
|
||||
RemettreAZero();
|
||||
Notifier();
|
||||
}
|
||||
|
||||
/// <summary>Reprend une valeur dans le champ : on la corrige là où on l'a écrite.</summary>
|
||||
private void Modifier(int index)
|
||||
{
|
||||
_message = null;
|
||||
_indexModifie = index;
|
||||
_saisie = Valeurs[index];
|
||||
}
|
||||
|
||||
private void Annuler() => RemettreAZero();
|
||||
|
||||
private void Supprimer(int index)
|
||||
{
|
||||
Valeurs.RemoveAt(index);
|
||||
|
||||
// La valeur qu'on modifiait vient peut-être de disparaître, et les rangs suivants ont
|
||||
// glissé : reprendre la modification écrirait sur la mauvaise.
|
||||
RemettreAZero();
|
||||
Notifier();
|
||||
}
|
||||
|
||||
private void Notifier() => _ = OnChangement.InvokeAsync();
|
||||
}
|
||||
Reference in New Issue
Block a user