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:
mathieu
2026-08-22 00:11:37 +02:00
co-authored by Claude Opus 5
parent e46fa99e3b
commit 75f962ab46
7 changed files with 419 additions and 264 deletions
@@ -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>